first draft

This commit is contained in:
einhorn_b 2023-10-23 19:07:13 +02:00
parent 53b2fe33a0
commit 2ff7adef52
79 changed files with 2092 additions and 555 deletions

View File

@ -0,0 +1 @@
declare module 'bip32-ed25519'

View File

@ -16,10 +16,11 @@
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --forceExit --detectOpenHandles"
},
"dependencies": {
"@apollo/protobufjs": "^1.2.7",
"@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",
@ -32,6 +33,7 @@
"graphql-scalars": "^1.22.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",

View File

@ -1,66 +0,0 @@
import 'reflect-metadata'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { create as createCommunity, getAllTopics, isExist } from './Community'
import { TestDB } from '@test/TestDB'
import { getDataSource } from '@/typeorm/DataSource'
import { Community } from '@entity/Community'
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
jest.mock('@typeorm/DataSource', () => ({
getDataSource: () => TestDB.instance.dbConnect,
}))
describe('controller/Community', () => {
beforeAll(async () => {
await TestDB.instance.setupTestDB()
// apolloTestServer = await createApolloTestServer()
})
afterAll(async () => {
await TestDB.instance.teardownTestDB()
})
describe('createCommunity', () => {
it('valid community', async () => {
const communityDraft = new CommunityDraft()
communityDraft.foreign = false
communityDraft.createdAt = '2022-05-01T17:00:12.128Z'
communityDraft.uuid = '3d813cbb-47fb-32ba-91df-831e1593ac29'
const iotaTopic = iotaTopicFromCommunityUUID(communityDraft.uuid)
expect(iotaTopic).toEqual('204ef6aed15fbf0f9da5819e88f8eea8e3adbe1e2c2d43280780a4b8c2d32b56')
const createdAtDate = new Date(communityDraft.createdAt)
const communityEntity = createCommunity(communityDraft)
expect(communityEntity).toMatchObject({
iotaTopic,
createdAt: createdAtDate,
foreign: false,
})
await getDataSource().manager.save(communityEntity)
})
})
describe('list communities', () => {
it('get all topics', async () => {
expect(await getAllTopics()).toMatchObject([
'204ef6aed15fbf0f9da5819e88f8eea8e3adbe1e2c2d43280780a4b8c2d32b56',
])
})
it('isExist with communityDraft', async () => {
const communityDraft = new CommunityDraft()
communityDraft.foreign = false
communityDraft.createdAt = '2022-05-01T17:00:12.128Z'
communityDraft.uuid = '3d813cbb-47fb-32ba-91df-831e1593ac29'
expect(await isExist(communityDraft)).toBe(true)
})
it('createdAt with ms precision', async () => {
const list = await Community.findOne({ where: { foreign: false } })
expect(list).toMatchObject({
createdAt: new Date('2022-05-01T17:00:12.128Z'),
})
})
})
})

View File

@ -1,28 +0,0 @@
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
import { Community } from '@entity/Community'
export const isExist = async (community: CommunityDraft | string): Promise<boolean> => {
const iotaTopic =
community instanceof CommunityDraft ? iotaTopicFromCommunityUUID(community.uuid) : community
const result = await Community.find({
where: { iotaTopic },
})
return result.length > 0
}
export const create = (community: CommunityDraft, topic?: string): Community => {
const communityEntity = Community.create()
communityEntity.iotaTopic = topic ?? iotaTopicFromCommunityUUID(community.uuid)
communityEntity.createdAt = new Date(community.createdAt)
communityEntity.foreign = community.foreign
if (!community.foreign) {
// TODO: generate keys
}
return communityEntity
}
export const getAllTopics = async (): Promise<string[]> => {
const communities = await Community.find({ select: { iotaTopic: true } })
return communities.map((community) => community.iotaTopic)
}

View File

@ -1,10 +0,0 @@
import { GradidoTransaction } from '@/proto/3_3/GradidoTransaction'
import { TransactionBody } from '@/proto/3_3/TransactionBody'
export const create = (body: TransactionBody): GradidoTransaction => {
const transaction = new GradidoTransaction({
bodyBytes: Buffer.from(TransactionBody.encode(body).finish()),
})
// TODO: add correct signature(s)
return transaction
}

View File

@ -0,0 +1,22 @@
/* eslint-disable camelcase */
import { entropyToMnemonic, mnemonicToSeedSync } from 'bip39'
import { generateFromSeed, toPublic } from 'bip32-ed25519'
describe('controller/KeyManager', () => {
describe('test crypto lib', () => {
it('key length', () => {
const mnemonic = entropyToMnemonic(
'aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899',
)
expect(mnemonic).toEqual(
'primary taxi danger target useless ancient match hammer fever crisp timber crew produce toy jeans that abandon math mimic master filter design carbon carbon',
)
const seed = mnemonicToSeedSync(mnemonic)
// private key 64 Bytes + 32 Byte ChainCode
const extendPrivkey = generateFromSeed(seed)
expect(extendPrivkey).toHaveLength(96)
// public key 32 Bytes + 32 Bytes ChainCode
expect(toPublic(extendPrivkey)).toHaveLength(64)
})
})
})

View File

@ -0,0 +1,123 @@
// eslint-disable-next-line camelcase
import { randombytes_buf } from 'sodium-native'
import { CONFIG } from '../config'
import { entropyToMnemonic, mnemonicToSeedSync } from 'bip39'
// https://www.npmjs.com/package/bip32-ed25519?activeTab=code
import {
generateFromSeed,
derivePrivate,
sign as ed25519Sign,
verify as ed25519Verify,
} from 'bip32-ed25519'
import { logger } from '@/server/logger'
import { LogError } from '@/server/LogError'
import { KeyPair } from '@/data/KeyPair'
import { GradidoTransaction } from '@/data/proto/3_3/GradidoTransaction'
import { CommunityRepository } from '@/data/Community.repository'
import { SignaturePair } from '@/data/proto/3_3/SignaturePair'
// Source: https://refactoring.guru/design-patterns/singleton/typescript/example
// and ../federation/client/FederationClientFactory.ts
/**
* A Singleton class defines the `getInstance` method that lets clients access
* the unique singleton instance.
*/
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class KeyManager {
// eslint-disable-next-line no-use-before-define
private static instance: KeyManager
private homeCommunityRootKeys: KeyPair | null = null
/**
* The Singleton's constructor should always be private to prevent direct
* construction calls with the `new` operator.
*/
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
private constructor() {}
/**
* The static method that controls the access to the singleton instance.
*
* This implementation let you subclass the Singleton class while keeping
* just one instance of each subclass around.
*/
public static getInstance(): KeyManager {
if (!KeyManager.instance) {
KeyManager.instance = new KeyManager()
}
return KeyManager.instance
}
public async init(): Promise<boolean> {
try {
this.homeCommunityRootKeys = await CommunityRepository.loadHomeCommunityKeyPair()
return true
} catch (error) {
logger.error('error by init key manager', error)
return false
}
}
public static generateKeyPair(): KeyPair {
const mnemonic = KeyManager.generateMnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED ?? undefined)
// logger.info('passphrase for key pair: ' + mnemonic)
const seed = mnemonicToSeedSync(mnemonic)
return new KeyPair(generateFromSeed(seed))
}
public setHomeCommunityKeyPair(keyPair: KeyPair) {
this.homeCommunityRootKeys = keyPair
}
public sign(transaction: GradidoTransaction, keys?: KeyPair[]) {
let localKeys: KeyPair[] = []
if (!keys && this.homeCommunityRootKeys) {
localKeys.push(this.homeCommunityRootKeys)
} else if (keys) {
localKeys = keys
}
if (!localKeys.length) {
throw new LogError('no key pair for signing')
}
localKeys.forEach((keyPair: KeyPair) => {
const signature = ed25519Sign(transaction.bodyBytes, keyPair.getExtendPrivateKey())
const sigPair = new SignaturePair({ pubKey: keyPair.publicKey, signature })
logger.debug('sign transaction', {
signature: signature.toString('hex'),
publicKey: keyPair.publicKey.toString('hex'),
bodyBytes: transaction.bodyBytes.toString('hex'),
})
transaction.sigMap.sigPair.push(sigPair)
})
}
public getHomeCommunityPublicKey(): Buffer | undefined {
if (!this.homeCommunityRootKeys) return undefined
return this.homeCommunityRootKeys.publicKey
}
public derive(path: number[], parentKeys?: KeyPair): KeyPair {
const extendedPrivateKey = parentKeys
? parentKeys.getExtendPrivateKey()
: this.homeCommunityRootKeys?.getExtendPrivateKey()
if (!extendedPrivateKey) {
throw new LogError('missing parent or root key pair')
}
return new KeyPair(
path.reduce(
(extendPrivateKey: Buffer, node: number) => derivePrivate(extendPrivateKey, node),
extendedPrivateKey,
),
)
}
static generateMnemonic(seed?: Buffer | string): string {
if (seed) {
return entropyToMnemonic(seed)
}
const entropy = Buffer.alloc(256)
randombytes_buf(entropy)
return entropyToMnemonic(entropy)
}
}

View File

@ -1,162 +0,0 @@
import 'reflect-metadata'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { create, determineCrossGroupType, determineOtherGroup } from './TransactionBody'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { TransactionError } from '@/graphql/model/TransactionError'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { CrossGroupType } from '@/graphql/enum/CrossGroupType'
import { TransactionType } from '@/graphql/enum/TransactionType'
import Decimal from 'decimal.js-light'
describe('test controller/TransactionBody', () => {
describe('test create ', () => {
const senderUser = new UserIdentifier()
const recipientUser = new UserIdentifier()
it('test with contribution transaction', () => {
const transactionDraft = new TransactionDraft()
transactionDraft.senderUser = senderUser
transactionDraft.recipientUser = recipientUser
transactionDraft.type = TransactionType.CREATION
transactionDraft.amount = new Decimal(1000)
transactionDraft.createdAt = '2022-01-02T19:10:34.121'
transactionDraft.targetDate = '2021-12-01T10:05:00.191'
const body = create(transactionDraft)
expect(body.creation).toBeDefined()
expect(body).toMatchObject({
createdAt: {
seconds: 1641150634,
nanoSeconds: 121000000,
},
versionNumber: '3.3',
type: CrossGroupType.LOCAL,
otherGroup: '',
creation: {
recipient: {
amount: '1000',
},
targetDate: {
seconds: 1638353100,
},
},
})
})
it('test with local send transaction send part', () => {
const transactionDraft = new TransactionDraft()
transactionDraft.senderUser = senderUser
transactionDraft.recipientUser = recipientUser
transactionDraft.type = TransactionType.SEND
transactionDraft.amount = new Decimal(1000)
transactionDraft.createdAt = '2022-01-02T19:10:34.121'
const body = create(transactionDraft)
expect(body.transfer).toBeDefined()
expect(body).toMatchObject({
createdAt: {
seconds: 1641150634,
nanoSeconds: 121000000,
},
versionNumber: '3.3',
type: CrossGroupType.LOCAL,
otherGroup: '',
transfer: {
sender: {
amount: '1000',
},
},
})
})
it('test with local send transaction receive part', () => {
const transactionDraft = new TransactionDraft()
transactionDraft.senderUser = senderUser
transactionDraft.recipientUser = recipientUser
transactionDraft.type = TransactionType.RECEIVE
transactionDraft.amount = new Decimal(1000)
transactionDraft.createdAt = '2022-01-02T19:10:34.121'
const body = create(transactionDraft)
expect(body.transfer).toBeDefined()
expect(body).toMatchObject({
createdAt: {
seconds: 1641150634,
nanoSeconds: 121000000,
},
versionNumber: '3.3',
type: CrossGroupType.LOCAL,
otherGroup: '',
transfer: {
sender: {
amount: '1000',
},
},
})
})
})
describe('test determineCrossGroupType', () => {
const transactionDraft = new TransactionDraft()
transactionDraft.senderUser = new UserIdentifier()
transactionDraft.recipientUser = new UserIdentifier()
it('local transaction', () => {
expect(determineCrossGroupType(transactionDraft)).toEqual(CrossGroupType.LOCAL)
})
it('test with with invalid input', () => {
transactionDraft.recipientUser.communityUuid = 'a72a4a4a-aa12-4f6c-b3d8-7cc65c67e24a'
expect(() => determineCrossGroupType(transactionDraft)).toThrow(
new TransactionError(
TransactionErrorType.NOT_IMPLEMENTED_YET,
'cannot determine CrossGroupType',
),
)
})
it('inbound transaction (send to sender community)', () => {
transactionDraft.type = TransactionType.SEND
expect(determineCrossGroupType(transactionDraft)).toEqual(CrossGroupType.INBOUND)
})
it('outbound transaction (send to recipient community)', () => {
transactionDraft.type = TransactionType.RECEIVE
expect(determineCrossGroupType(transactionDraft)).toEqual(CrossGroupType.OUTBOUND)
})
})
describe('test determineOtherGroup', () => {
const transactionDraft = new TransactionDraft()
transactionDraft.senderUser = new UserIdentifier()
transactionDraft.recipientUser = new UserIdentifier()
it('for inbound transaction, other group is from recipient, missing community id for recipient', () => {
expect(() => determineOtherGroup(CrossGroupType.INBOUND, transactionDraft)).toThrowError(
new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing recipient user community id for cross group transaction',
),
)
})
it('for inbound transaction, other group is from recipient', () => {
transactionDraft.recipientUser.communityUuid = 'b8e9f00a-5a56-4b23-8c44-6823ac9e0d2d'
expect(determineOtherGroup(CrossGroupType.INBOUND, transactionDraft)).toEqual(
'b8e9f00a-5a56-4b23-8c44-6823ac9e0d2d',
)
})
it('for outbound transaction, other group is from sender, missing community id for sender', () => {
expect(() => determineOtherGroup(CrossGroupType.OUTBOUND, transactionDraft)).toThrowError(
new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing sender user community id for cross group transaction',
),
)
})
it('for outbound transaction, other group is from sender', () => {
transactionDraft.senderUser.communityUuid = 'a72a4a4a-aa12-4f6c-b3d8-7cc65c67e24a'
expect(determineOtherGroup(CrossGroupType.OUTBOUND, transactionDraft)).toEqual(
'a72a4a4a-aa12-4f6c-b3d8-7cc65c67e24a',
)
})
})
})

View File

@ -0,0 +1,33 @@
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { getDataSource } from '@/typeorm/DataSource'
import { Account } from '@entity/Account'
import { User } from '@entity/User'
import { In } from 'typeorm'
export const AccountRepository = getDataSource()
.getRepository(Account)
.extend({
findAccountsByPublicKeys(publicKeys: Buffer[]): Promise<Account[]> {
return Account.findBy({ derive2Pubkey: In(publicKeys) })
},
async findAccountByPublicKey(publicKey: Buffer | undefined): Promise<Account | undefined> {
if (!publicKey) return undefined
return (await Account.findOneBy({ derive2Pubkey: Buffer.from(publicKey) })) ?? undefined
},
async findAccountByUserIdentifier({
uuid,
accountNr,
}: UserIdentifier): Promise<Account | undefined> {
const user = await User.findOne({
where: { gradidoID: uuid, accounts: { derivationIndex: accountNr ?? 1 } },
relations: { accounts: true },
})
if (user && user.accounts?.length === 1) {
const account = user.accounts[0]
account.user = user
return account
}
},
})

View File

@ -0,0 +1,74 @@
import { CommunityArg } from '@/graphql/arg/CommunityArg'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { TransactionError } from '@/graphql/model/TransactionError'
import { getDataSource } from '@/typeorm/DataSource'
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
import { Community } from '@entity/Community'
import { FindOptionsSelect, In, IsNull, Not } from 'typeorm'
import { KeyPair } from './KeyPair'
import { LogError } from '@/server/LogError'
export const CommunityRepository = getDataSource()
.getRepository(Community)
.extend({
async isExist(community: CommunityDraft | string): Promise<boolean> {
const iotaTopic =
community instanceof CommunityDraft ? iotaTopicFromCommunityUUID(community.uuid) : community
const result = await Community.find({
where: { iotaTopic },
})
return result.length > 0
},
async findByCommunityArg({ uuid, foreign, confirmed }: CommunityArg): Promise<Community[]> {
return await Community.find({
where: {
...(uuid && { iotaTopic: iotaTopicFromCommunityUUID(uuid) }),
...(foreign && { foreign }),
...(confirmed && { confirmedAt: Not(IsNull()) }),
},
})
},
async findByCommunityUuid(communityUuid: string): Promise<Community | null> {
return await Community.findOneBy({ iotaTopic: iotaTopicFromCommunityUUID(communityUuid) })
},
async findByIotaTopic(iotaTopic: string): Promise<Community | null> {
return await Community.findOneBy({ iotaTopic })
},
findCommunitiesByTopics(topics: string[]): Promise<Community[]> {
return Community.findBy({ iotaTopic: In(topics) })
},
async getCommunityForUserIdentifier(
identifier: UserIdentifier,
): Promise<Community | undefined> {
if (!identifier.communityUuid) {
throw new TransactionError(TransactionErrorType.MISSING_PARAMETER, 'community uuid not set')
}
return (
(await Community.findOneBy({
iotaTopic: iotaTopicFromCommunityUUID(identifier.communityUuid),
})) ?? undefined
)
},
findAll(select: FindOptionsSelect<Community>): Promise<Community[]> {
return Community.find({ select })
},
async loadHomeCommunityKeyPair(): Promise<KeyPair> {
const community = await Community.findOneOrFail({
where: { foreign: false },
select: { rootChaincode: true, rootPubkey: true, rootPrivkey: true },
})
if (!community.rootChaincode || !community.rootPrivkey) {
throw new LogError('Missing chaincode or private key for home community')
}
return new KeyPair(community)
},
})

View File

@ -0,0 +1,38 @@
// https://www.npmjs.com/package/bip32-ed25519?activeTab=code
import { toPublic } from 'bip32-ed25519'
import { Community } from '@entity/Community'
import { LogError } from '@/server/LogError'
export class KeyPair {
/**
* @param input: Buffer = extended private key, returned from bip32-ed25519 generateFromSeed
* @param input: Community = community entity with keys loaded from db
*
*/
public constructor(input: Buffer | Community) {
if (input instanceof Buffer) {
this.privateKey = input.subarray(0, 64)
this.chainCode = input.subarray(64, 96)
this.publicKey = toPublic(input).subarray(0, 32)
} 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')
}
this.privateKey = input.rootPrivkey
this.publicKey = input.rootPubkey
this.chainCode = input.rootChaincode
}
}
public getExtendPrivateKey(): Buffer {
return Buffer.concat([this.privateKey, this.chainCode])
}
public getExtendPublicKey(): Buffer {
return Buffer.concat([this.publicKey, this.chainCode])
}
publicKey: Buffer
chainCode: Buffer
privateKey: Buffer
}

View File

@ -0,0 +1,147 @@
import { GradidoTransaction } from '@/data/proto/3_3/GradidoTransaction'
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
import { bodyBytesToTransactionBody, transactionBodyToBodyBytes } from '@/utils/typeConverter'
import { Transaction } from '@entity/Transaction'
import { AccountRepository } from './Account.repository'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { CommunityRepository } from './Community.repository'
import { LogError } from '@/server/LogError'
import { Account } from '@entity/Account'
import { Community } from '@entity/Community'
export class TransactionBuilder {
private transaction: Transaction
// 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.transaction = Transaction.create()
}
/**
* 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(): Transaction {
const result = this.transaction
this.reset()
return result
}
// return transaction without calling reset
public getTransaction(): Transaction {
return this.transaction
}
public setSigningAccount(signingAccount: Account): TransactionBuilder {
this.transaction.signingAccount = signingAccount
return this
}
public setRecipientAccount(recipientAccount: Account): TransactionBuilder {
this.transaction.recipientAccount = recipientAccount
return this
}
public setSenderCommunity(senderCommunity: Community): TransactionBuilder {
this.transaction.senderCommunity = senderCommunity
return this
}
public setRecipientCommunity(recipientCommunity?: Community): TransactionBuilder {
if (!this.transaction.senderCommunity) {
throw new LogError('Please set sender community first!')
}
this.transaction.recipientCommunity =
recipientCommunity &&
this.transaction.senderCommunity &&
this.transaction.senderCommunity.id !== recipientCommunity.id
? recipientCommunity
: undefined
return this
}
public setSignature(signature: Buffer): TransactionBuilder {
this.transaction.signature = signature
return this
}
public async setSenderCommunityFromSenderUser(
senderUser: UserIdentifier,
): Promise<TransactionBuilder> {
// get sender community
const senderCommunity = await CommunityRepository.getCommunityForUserIdentifier(senderUser)
if (!senderCommunity) {
throw new LogError("couldn't find sender community for transaction")
}
return this.setSenderCommunity(senderCommunity)
}
public async setRecipientCommunityFromRecipientUser(
recipientUser: UserIdentifier,
): Promise<TransactionBuilder> {
// get recipient community
const recipientCommunity = await CommunityRepository.getCommunityForUserIdentifier(
recipientUser,
)
return this.setRecipientCommunity(recipientCommunity)
}
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(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
}
}

View File

@ -0,0 +1,48 @@
import { getDataSource } from '@/typeorm/DataSource'
import { Transaction } from '@entity/Transaction'
import { IsNull } from 'typeorm'
// https://www.artima.com/articles/the-dci-architecture-a-new-vision-of-object-oriented-programming
export const TransactionRepository = getDataSource()
.getRepository(Transaction)
.extend({
findBySignature(signature: Buffer): Promise<Transaction | null> {
return this.findOneBy({ signature: Buffer.from(signature) })
},
findByMessageId(iotaMessageId: string): Promise<Transaction | null> {
return this.findOneBy({ iotaMessageId: Buffer.from(iotaMessageId, 'hex') })
},
async getNextPendingTransaction(): Promise<Transaction | null> {
return await this.findOne({
where: { iotaMessageId: IsNull() },
order: { createdAt: 'ASC' },
relations: { signingAccount: true },
})
},
async findExistingTransactionAndMissingMessageIds(messageIDsHex: string[]): Promise<{
existingTransactions: Transaction[]
missingMessageIdsHex: string[]
}> {
const existingTransactions = await this.createQueryBuilder('Transaction')
.where('HEX(Transaction.iota_message_id) IN (:...messageIDs)', {
messageIDs: messageIDsHex,
})
.leftJoinAndSelect('Transaction.recipientAccount', 'RecipientAccount')
.leftJoinAndSelect('RecipientAccount.user', 'RecipientUser')
.leftJoinAndSelect('Transaction.signingAccount', 'SigningAccount')
.leftJoinAndSelect('SigningAccount.user', 'SigningUser')
.getMany()
const foundMessageIds = existingTransactions
.map((recipe) => recipe.iotaMessageId?.toString('hex'))
.filter((messageId) => !!messageId)
// find message ids for which we don't already have a transaction recipe
const missingMessageIdsHex = messageIDsHex.filter(
(id: string) => !foundMessageIds.includes(id),
)
return { existingTransactions, missingMessageIdsHex }
},
async removeConfirmedTransaction(transactions: Transaction[]): Promise<Transaction[]> {
return transactions.filter((transaction: Transaction) => transaction.runningHash.length === 0)
},
})

View File

@ -0,0 +1,42 @@
import { KeyManager } from '@/controller/KeyManager'
import { KeyPair } from '@/data/KeyPair'
import { AddressType } from '@/data/proto/3_3/enum/AddressType'
import { hardenDerivationIndex } from '@/utils/derivationHelper'
import { Account } from '@entity/Account'
import Decimal from 'decimal.js-light'
const GMW_ACCOUNT_DERIVATION_INDEX = 1
const AUF_ACCOUNT_DERIVATION_INDEX = 2
export const createAccount = (
keyPair: KeyPair,
createdAt: Date,
derivationIndex: number,
type: AddressType,
): Account => {
const account = Account.create()
account.derivationIndex = derivationIndex
account.derive2Pubkey = KeyManager.getInstance().derive([derivationIndex], keyPair).publicKey
account.type = type.valueOf()
account.createdAt = createdAt
account.balance = new Decimal(0)
return account
}
export const createGmwAccount = (keyPair: KeyPair, createdAt: Date): Account => {
return createAccount(
keyPair,
createdAt,
hardenDerivationIndex(GMW_ACCOUNT_DERIVATION_INDEX),
AddressType.COMMUNITY_GMW,
)
}
export const createAufAccount = (keyPair: KeyPair, createdAt: Date): Account => {
return createAccount(
keyPair,
createdAt,
hardenDerivationIndex(AUF_ACCOUNT_DERIVATION_INDEX),
AddressType.COMMUNITY_AUF,
)
}

View File

@ -0,0 +1,31 @@
import { KeyManager } from '@/controller/KeyManager'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
import { Community } from '@entity/Community'
import { createAufAccount, createGmwAccount } from './account.factory'
export const createCommunity = (communityDraft: CommunityDraft, topic?: string): Community => {
const communityEntity = Community.create()
communityEntity.iotaTopic = topic ?? iotaTopicFromCommunityUUID(communityDraft.uuid)
communityEntity.createdAt = new Date(communityDraft.createdAt)
communityEntity.foreign = communityDraft.foreign
return communityEntity
}
export const createHomeCommunity = (communityDraft: CommunityDraft, topic?: string): Community => {
// create community entity
const community = createCommunity(communityDraft, topic)
// generate key pair for signing transactions and deriving all keys for community
const keyPair = KeyManager.generateKeyPair()
community.rootPubkey = keyPair.publicKey
community.rootPrivkey = keyPair.privateKey
community.rootChaincode = keyPair.chainCode
// we should only have one home community per server
KeyManager.getInstance().setHomeCommunityKeyPair(keyPair)
// create auf account and gmw account
community.aufAccount = createAufAccount(keyPair, community.createdAt)
community.gmwAccount = createGmwAccount(keyPair, community.createdAt)
return community
}

View File

@ -0,0 +1,40 @@
import { TransactionBase } from '../TransactionBase'
import { TransactionValidationLevel } from '@/graphql/enum/TransactionValidationLevel'
import { Field, Message } from 'protobufjs'
import { Community } from '@entity/Community'
import { Transaction } from '@entity/Transaction'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class CommunityRoot extends Message<CommunityRoot> implements TransactionBase {
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-unused-vars
public validate(_level: TransactionValidationLevel): boolean {
throw new Error('Method not implemented.')
}
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
public fillTransactionRecipe(recipe: Transaction): void {}
}

View File

@ -1,6 +1,8 @@
import { Field, Message } from '@apollo/protobufjs'
import { Field, Message } from 'protobufjs'
import { GradidoTransaction } from './GradidoTransaction'
import { TimestampSeconds } from './TimestampSeconds'
import { base64ToBuffer } from '@/utils/typeConverter'
import Long from 'long'
/*
id will be set by Node server
@ -10,9 +12,13 @@ import { TimestampSeconds } from './TimestampSeconds'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class GradidoConfirmedTransaction extends Message<GradidoConfirmedTransaction> {
export class ConfirmedTransaction extends Message<ConfirmedTransaction> {
static fromBase64(base64: string): ConfirmedTransaction {
return ConfirmedTransaction.decode(new Uint8Array(base64ToBuffer(base64)))
}
@Field.d(1, 'uint64')
id: number
id: Long
@Field.d(2, 'GradidoTransaction')
transaction: GradidoTransaction

View File

@ -0,0 +1,53 @@
import { Field, Message } from 'protobufjs'
import { TimestampSeconds } from './TimestampSeconds'
import { TransferAmount } from './TransferAmount'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionBase } from '../TransactionBase'
import { TransactionValidationLevel } from '@/graphql/enum/TransactionValidationLevel'
import { Transaction } from '@entity/Transaction'
import Decimal from 'decimal.js-light'
import { Account } from '@entity/Account'
// 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 TransactionBase {
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()
}
}
@Field.d(1, TransferAmount)
public recipient: TransferAmount
@Field.d(3, 'TimestampSeconds')
public targetDate: TimestampSeconds
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public validate(level: TransactionValidationLevel): boolean {
throw new Error('Method not implemented.')
}
public fillTransactionRecipe(recipe: Transaction): void {
recipe.amount = new Decimal(this.recipient.amount ?? 0)
}
}

View File

@ -1,7 +1,11 @@
import { Field, Message } from '@apollo/protobufjs'
import { Field, Message } from 'protobufjs'
import { GradidoTransfer } from './GradidoTransfer'
import { TimestampSeconds } from './TimestampSeconds'
import { TransactionBase } from '../TransactionBase'
import { TransactionValidationLevel } from '@/graphql/enum/TransactionValidationLevel'
import { Transaction } from '@entity/Transaction'
import Decimal from 'decimal.js-light'
// transaction type for chargeable transactions
// for transaction for people which haven't a account already
@ -10,8 +14,11 @@ import { TimestampSeconds } from './TimestampSeconds'
// seed must be long enough to prevent brute force, maybe base64 encoded
// to own account
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class GradidoDeferredTransfer extends Message<GradidoDeferredTransfer> {
export class GradidoDeferredTransfer
// eslint-disable-next-line no-use-before-define
extends Message<GradidoDeferredTransfer>
implements TransactionBase
{
// 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
@ -28,4 +35,13 @@ export class GradidoDeferredTransfer extends Message<GradidoDeferredTransfer> {
// split for n recipient
// max gradido per recipient? or per transaction with cool down?
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public validate(level: TransactionValidationLevel): boolean {
throw new Error('Method not implemented.')
}
public fillTransactionRecipe(recipe: Transaction): void {
recipe.amount = new Decimal(this.transfer.sender.amount ?? 0)
}
}

View File

@ -0,0 +1,43 @@
import { Field, Message } from 'protobufjs'
import { SignatureMap } from './SignatureMap'
import { TransactionBody } from './TransactionBody'
import { SignaturePair } from './SignaturePair'
import { LogError } from '@/server/LogError'
// 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]
}
}

View File

@ -0,0 +1,48 @@
import { Field, Message } from 'protobufjs'
import { TransferAmount } from './TransferAmount'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { TransactionBase } from '../TransactionBase'
import { TransactionValidationLevel } from '@/graphql/enum/TransactionValidationLevel'
import { Transaction } from '@entity/Transaction'
import Decimal from 'decimal.js-light'
import { Account } from '@entity/Account'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class GradidoTransfer extends Message<GradidoTransfer> implements TransactionBase {
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()
}
}
@Field.d(1, TransferAmount)
public sender: TransferAmount
@Field.d(2, 'bytes')
public recipient: Buffer
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public validate(level: TransactionValidationLevel): boolean {
throw new Error('Method not implemented.')
}
public fillTransactionRecipe(recipe: Transaction): void {
recipe.amount = new Decimal(this.sender?.amount ?? 0)
}
}

View File

@ -1,10 +1,14 @@
import { Field, Message } from '@apollo/protobufjs'
/* eslint-disable @typescript-eslint/no-unused-vars */
import { TransactionBase } from '../TransactionBase'
import { TransactionValidationLevel } from '@/graphql/enum/TransactionValidationLevel'
import { Field, Message } from 'protobufjs'
import { Transaction } from '@entity/Transaction'
// 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> {
export class GroupFriendsUpdate extends Message<GroupFriendsUpdate> implements TransactionBase {
// 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,
@ -12,4 +16,12 @@ export class GroupFriendsUpdate extends Message<GroupFriendsUpdate> {
// (if fusion between src coin and dst coin is enabled)
@Field.d(1, 'bool')
public colorFusion: boolean
public validate(level: TransactionValidationLevel): boolean {
throw new Error('Method not implemented.')
}
public fillTransactionRecipe(recipe: Transaction): void {
throw new Error('Method not implemented.')
}
}

View File

@ -0,0 +1,33 @@
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Field, Message } from 'protobufjs'
import { AddressType } from '@/data/proto/3_3/enum/AddressType'
import { TransactionBase } from '../TransactionBase'
import { TransactionValidationLevel } from '@/graphql/enum/TransactionValidationLevel'
import { Transaction } from '@entity/Transaction'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class RegisterAddress extends Message<RegisterAddress> implements TransactionBase {
@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 validate(level: TransactionValidationLevel): boolean {
throw new Error('Method not implemented.')
}
public fillTransactionRecipe(_recipe: Transaction): void {}
}

View File

@ -1,10 +1,14 @@
import { Field, Message } from '@apollo/protobufjs'
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
public sigPair: SignaturePair[]
}

View File

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

View File

@ -1,4 +1,4 @@
import { Field, Message } from '@apollo/protobufjs'
import { Field, Message } from 'protobufjs'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define

View File

@ -1,4 +1,4 @@
import { Field, Message } from '@apollo/protobufjs'
import { Field, Message } from 'protobufjs'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define

View File

@ -0,0 +1,129 @@
import { Field, Message, OneOf } from 'protobufjs'
import { CrossGroupType } from './enum/CrossGroupType'
import { Timestamp } from './Timestamp'
import { GradidoTransfer } from './GradidoTransfer'
import { GradidoCreation } from './GradidoCreation'
import { GradidoDeferredTransfer } from './GradidoDeferredTransfer'
import { GroupFriendsUpdate } from './GroupFriendsUpdate'
import { RegisterAddress } from './RegisterAddress'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { determineCrossGroupType, determineOtherGroup } from '../transactionBody.logic'
import { CommunityRoot } from './CommunityRoot'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { TransactionType } from '@/graphql/enum/TransactionType'
import { TransactionBase } from '../TransactionBase'
import { Transaction } from '@entity/Transaction'
import { timestampToDate } from '@/utils/typeConverter'
import { LogError } from '@/server/LogError'
// 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) {
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: '3.3',
type,
otherGroup,
})
} else {
super()
}
}
@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
}
public getTransactionBase(): TransactionBase | 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.getTransactionBase()?.fillTransactionRecipe(recipe)
}
public getRecipientPublicKey(): Buffer | undefined {
if (this.transfer) {
return this.transfer.recipient
}
if (this.creation) {
return this.creation.recipient.pubkey
}
if (this.deferredTransfer) {
return this.deferredTransfer.transfer.recipient
}
return undefined
}
}

View File

@ -1,4 +1,4 @@
import { Field, Message } from '@apollo/protobufjs'
import { Field, Message } from 'protobufjs'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define

View File

@ -0,0 +1,19 @@
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
}
export function getAddressTypeEnumValue(typeString: string): AddressType | undefined {
// Iterate through all enum values
for (const key in AddressType) {
if (AddressType[key] === typeString) {
return AddressType[key] as unknown as AddressType
}
}
return undefined // If the string is not found
}

View File

@ -0,0 +1,7 @@
export enum CrossGroupType {
LOCAL = 0,
INBOUND = 1,
OUTBOUND = 2,
// for cross group transaction which haven't a direction like group friend update
// CROSS = 3,
}

View File

@ -1,6 +1,9 @@
import { TransactionValidationLevel } from '@/graphql/enum/TransactionValidationLevel'
import { Transaction } from '@entity/Transaction'
export abstract class TransactionBase {
// validate if transaction is valid, maybe expensive because depending on level several transactions will be fetched from db
public abstract validate(level: TransactionValidationLevel): boolean
public abstract fillTransactionRecipe(recipe: Transaction): void
}

View File

@ -0,0 +1,95 @@
import { TransactionBody } from './3_3/TransactionBody'
import { Account } from '@entity/Account'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { GradidoCreation } from './3_3/GradidoCreation'
import { GradidoTransfer } from './3_3/GradidoTransfer'
import { Community } from '@entity/Community'
import { CommunityRoot } from './3_3/CommunityRoot'
import { LogError } from '@/server/LogError'
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
}
/**
* 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.body
if (!result) {
throw new LogError(
'cannot build Transaction Body, missing information, please call at least fromTransactionDraft or fromCommunityDraft',
)
}
this.reset()
return result
}
public setSigningAccount(signingAccount: Account): TransactionBodyBuilder {
this.signingAccount = signingAccount
return this
}
public setRecipientAccount(recipientAccount: Account): TransactionBodyBuilder {
this.recipientAccount = recipientAccount
return this
}
public fromTransactionDraft(transactionDraft: TransactionDraft): TransactionBodyBuilder {
this.body = new TransactionBody(transactionDraft)
// TODO: load pubkeys for sender and recipient user from db
switch (transactionDraft.type) {
case InputTransactionType.CREATION:
this.body.creation = new GradidoCreation(transactionDraft, this.recipientAccount)
this.body.data = 'gradidoCreation'
break
case InputTransactionType.SEND:
case InputTransactionType.RECEIVE:
this.body.transfer = new GradidoTransfer(
transactionDraft,
this.signingAccount,
this.recipientAccount,
)
this.body.data = 'gradidoTransfer'
break
}
return this
}
public fromCommunityDraft(
communityDraft: CommunityDraft,
community: Community,
): TransactionBodyBuilder {
this.body = new TransactionBody(communityDraft)
this.body.communityRoot = new CommunityRoot(community)
this.body.data = 'communityRoot'
return this
}
}

View File

@ -1,28 +1,8 @@
import { CrossGroupType } from '@/graphql/enum/CrossGroupType'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionType } from '@/graphql/enum/TransactionType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { CrossGroupType } from './3_3/enum/CrossGroupType'
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { TransactionError } from '@/graphql/model/TransactionError'
import { GradidoCreation } from '@/proto/3_3/GradidoCreation'
import { GradidoTransfer } from '@/proto/3_3/GradidoTransfer'
import { TransactionBody } from '@/proto/3_3/TransactionBody'
export const create = (transaction: TransactionDraft): TransactionBody => {
const body = new TransactionBody(transaction)
// TODO: load pubkeys for sender and recipient user from db
switch (transaction.type) {
case TransactionType.CREATION:
body.creation = new GradidoCreation(transaction)
body.data = 'gradidoCreation'
break
case TransactionType.SEND:
case TransactionType.RECEIVE:
body.transfer = new GradidoTransfer(transaction)
body.data = 'gradidoTransfer'
break
}
return body
}
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
export const determineCrossGroupType = ({
senderUser,
@ -33,12 +13,12 @@ export const determineCrossGroupType = ({
!recipientUser.communityUuid ||
recipientUser.communityUuid === '' ||
senderUser.communityUuid === recipientUser.communityUuid ||
type === TransactionType.CREATION
type === InputTransactionType.CREATION
) {
return CrossGroupType.LOCAL
} else if (type === TransactionType.SEND) {
} else if (type === InputTransactionType.SEND) {
return CrossGroupType.INBOUND
} else if (type === TransactionType.RECEIVE) {
} else if (type === InputTransactionType.RECEIVE) {
return CrossGroupType.OUTBOUND
}
throw new TransactionError(

View File

@ -0,0 +1,19 @@
// https://www.npmjs.com/package/@apollo/protobufjs
import { IsBoolean, IsUUID } from 'class-validator'
import { ArgsType, Field } from 'type-graphql'
@ArgsType()
export class CommunityArg {
@Field(() => String, { nullable: true })
@IsUUID('4')
uuid?: string
@Field(() => Boolean, { nullable: true })
@IsBoolean()
foreign?: boolean
@Field(() => Boolean, { nullable: true })
@IsBoolean()
confirmed?: boolean
}

View File

@ -0,0 +1,12 @@
import { registerEnumType } from 'type-graphql'
export enum InputTransactionType {
CREATION = 1,
SEND = 2,
RECEIVE = 3,
}
registerEnumType(InputTransactionType, {
name: 'InputTransactionType', // this one is mandatory
description: 'Type of the transaction', // this one is optional
})

View File

@ -5,6 +5,8 @@ export enum TransactionErrorType {
MISSING_PARAMETER = 'Missing parameter',
ALREADY_EXIST = 'Already exist',
DB_ERROR = 'DB Error',
PROTO_DECODE_ERROR = 'Proto Decode Error',
PROTO_ENCODE_ERROR = 'Proto Encode Error',
}
registerEnumType(TransactionErrorType, {

27
dlt-connector/src/graphql/enum/TransactionType.ts Executable file → Normal file
View File

@ -1,12 +1,15 @@
import { registerEnumType } from 'type-graphql'
export enum TransactionType {
CREATION = 1,
SEND = 2,
RECEIVE = 3,
}
registerEnumType(TransactionType, {
name: 'TransactionType', // this one is mandatory
description: 'Type of the transaction', // this one is optional
})
import { registerEnumType } from 'type-graphql'
export enum TransactionType {
GRADIDO_TRANSFER = 1,
GRADIDO_CREATION = 2,
GROUP_FRIENDS_UPDATE = 3,
REGISTER_ADDRESS = 4,
GRADIDO_DEFERRED_TRANSFER = 5,
COMMUNITY_ROOT = 6,
}
registerEnumType(TransactionType, {
name: 'TransactionType', // this one is mandatory
description: 'Type of the transaction', // this one is optional
})

View File

@ -1,7 +1,7 @@
// https://www.npmjs.com/package/@apollo/protobufjs
import { Decimal } from 'decimal.js-light'
import { TransactionType } from '@enum/TransactionType'
import { InputTransactionType } from '@enum/InputTransactionType'
import { InputType, Field } from 'type-graphql'
import { UserIdentifier } from './UserIdentifier'
import { isValidDateString } from '@validator/DateString'
@ -24,9 +24,9 @@ export class TransactionDraft {
@IsPositiveDecimal()
amount: Decimal
@Field(() => TransactionType)
@IsEnum(TransactionType)
type: TransactionType
@Field(() => InputTransactionType)
@IsEnum(InputTransactionType)
type: InputTransactionType
@Field(() => String)
@isValidDateString()

View File

@ -0,0 +1,36 @@
import { ObjectType, Field, Int } from 'type-graphql'
import { Community as CommunityEntity } from '@entity/Community'
@ObjectType()
export class Community {
constructor(entity: CommunityEntity) {
this.id = entity.id
this.iotaTopic = entity.iotaTopic
if (entity.rootPubkey) {
this.rootPublicKeyHex = entity.rootPubkey?.toString('hex')
}
this.foreign = entity.foreign
this.createdAt = entity.createdAt.toString()
if (entity.confirmedAt) {
this.confirmedAt = entity.confirmedAt.toString()
}
}
@Field(() => Int)
id: number
@Field(() => String)
iotaTopic: string
@Field(() => String)
rootPublicKeyHex?: string
@Field(() => Boolean)
foreign: boolean
@Field(() => String)
createdAt: string
@Field(() => String)
confirmedAt?: string
}

View File

@ -0,0 +1,25 @@
import { Field, Int, ObjectType } from 'type-graphql'
import { TransactionRecipe as TransactionRecipeEntity } from '@entity/TransactionRecipe'
import { TransactionType } from '@enum/TransactionType'
@ObjectType()
export class TransactionRecipe {
public constructor({ id, createdAt, type, senderCommunity }: TransactionRecipeEntity) {
this.id = id
this.createdAt = createdAt.toString()
this.type = type
this.topic = senderCommunity.iotaTopic
}
@Field(() => Int)
id: number
@Field(() => String)
createdAt: string
@Field(() => TransactionType)
type: TransactionType
@Field(() => String)
topic: string
}

View File

@ -1,15 +1,16 @@
import { ObjectType, Field } from 'type-graphql'
import { TransactionError } from './TransactionError'
import { TransactionRecipe } from './TransactionRecipe'
@ObjectType()
export class TransactionResult {
constructor(content?: TransactionError | string) {
constructor(content?: TransactionError | TransactionRecipe) {
this.succeed = true
if (content instanceof TransactionError) {
this.error = content
this.succeed = false
} else if (typeof content === 'string') {
this.messageId = content
} else if (content instanceof TransactionRecipe) {
this.recipe = content
}
}
@ -18,9 +19,9 @@ export class TransactionResult {
error?: TransactionError
// if no error happend, the message id of the iota transaction
@Field(() => String, { nullable: true })
messageId?: string
@Field(() => TransactionRecipe, { nullable: true })
recipe?: TransactionRecipe
@Field(() => Boolean)
succeed: boolean
}
}

View File

@ -1,53 +1,59 @@
import { Resolver, Arg, Mutation } from 'type-graphql'
import { Resolver, Query, Arg, Mutation, Args } from 'type-graphql'
import { CommunityDraft } from '@input/CommunityDraft'
import { TransactionResult } from '../model/TransactionResult'
import { TransactionError } from '../model/TransactionError'
import { create as createCommunity, isExist } from '@/controller/Community'
import { TransactionErrorType } from '../enum/TransactionErrorType'
import { logger } from '@/server/logger'
import { TransactionResult } from '@model/TransactionResult'
import { TransactionError } from '@model/TransactionError'
import { TransactionErrorType } from '@enum/TransactionErrorType'
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
import { Community } from '@model/Community'
import { CommunityArg } from '@arg/CommunityArg'
import { LogError } from '@/server/LogError'
import { logger } from '@/server/logger'
import { CommunityRepository } from '@/data/Community.repository'
import { addCommunity } from '@/interactions/backendToDb/community/community.context'
@Resolver()
export class CommunityResolver {
@Query(() => Community)
async community(@Args() communityArg: CommunityArg): Promise<Community> {
logger.info('community', communityArg)
const result = await CommunityRepository.findByCommunityArg(communityArg)
if (result.length === 0) {
throw new LogError('cannot find community')
} else if (result.length === 1) {
return new Community(result[0])
} else {
throw new LogError('find multiple communities')
}
}
@Query(() => Boolean)
async isCommunityExist(@Args() communityArg: CommunityArg): Promise<boolean> {
logger.info('isCommunity', communityArg)
return (await CommunityRepository.findByCommunityArg(communityArg)).length === 1
}
@Query(() => [Community])
async communities(@Args() communityArg: CommunityArg): Promise<Community[]> {
logger.info('communities', communityArg)
const result = await CommunityRepository.findByCommunityArg(communityArg)
return result.map((communityEntity) => new Community(communityEntity))
}
@Mutation(() => TransactionResult)
async addCommunity(
@Arg('data')
communityDraft: CommunityDraft,
): Promise<TransactionResult> {
try {
const topic = iotaTopicFromCommunityUUID(communityDraft.uuid)
// check if community was already written to db
if (await isExist(topic)) {
return new TransactionResult(
new TransactionError(TransactionErrorType.ALREADY_EXIST, 'community already exist!'),
)
}
const community = createCommunity(communityDraft, topic)
let result: TransactionResult
if (!communityDraft.foreign) {
// TODO: CommunityRoot Transaction for blockchain
}
try {
await community.save()
result = new TransactionResult()
} catch (err) {
logger.error('error saving new community into db: %s', err)
result = new TransactionResult(
new TransactionError(TransactionErrorType.DB_ERROR, 'error saving community into db'),
)
}
return result
} catch (error) {
if (error instanceof TransactionError) {
return new TransactionResult(error)
} else {
throw error
}
logger.info('addCommunity', communityDraft)
const topic = iotaTopicFromCommunityUUID(communityDraft.uuid)
// check if community was already written to db
if (await CommunityRepository.isExist(topic)) {
return new TransactionResult(
new TransactionError(TransactionErrorType.ALREADY_EXIST, 'community already exist!'),
)
}
return await addCommunity(communityDraft, topic)
}
}

View File

@ -1,12 +1,5 @@
import { Resolver, Query, Arg, Mutation } from 'type-graphql'
import { TransactionDraft } from '@input/TransactionDraft'
import { create as createTransactionBody } from '@controller/TransactionBody'
import { create as createGradidoTransaction } from '@controller/GradidoTransaction'
import { sendMessage as iotaSendMessage } from '@/client/IotaClient'
import { GradidoTransaction } from '@/proto/3_3/GradidoTransaction'
import { TransactionResult } from '../model/TransactionResult'
import { TransactionError } from '../model/TransactionError'
@ -29,11 +22,68 @@ export class TransactionResolver {
transaction: TransactionDraft,
): Promise<TransactionResult> {
try {
const body = createTransactionBody(transaction)
const message = createGradidoTransaction(body)
const messageBuffer = GradidoTransaction.encode(message).finish()
const resultMessage = await iotaSendMessage(messageBuffer)
return new TransactionResult(resultMessage.messageId)
logger.info('sendTransaction call', transaction)
const signingAccount = await findAccountByUserIdentifier(transaction.senderUser)
if (!signingAccount) {
throw new TransactionError(
TransactionErrorType.NOT_FOUND,
"couldn't found sender user account in db",
)
}
logger.info('signing account', signingAccount)
const recipientAccount = await findAccountByUserIdentifier(transaction.recipientUser)
if (!recipientAccount) {
throw new TransactionError(
TransactionErrorType.NOT_FOUND,
"couldn't found recipient user account in db",
)
}
logger.info('recipient account', recipientAccount)
const body = createTransactionBody(transaction, signingAccount, recipientAccount)
logger.info('body', body)
const gradidoTransaction = createGradidoTransaction(body)
const signingKeyPair = getKeyPair(signingAccount)
if (!signingKeyPair) {
throw new TransactionError(
TransactionErrorType.NOT_FOUND,
"couldn't found signing key pair",
)
}
logger.info('key pair for signing', signingKeyPair)
KeyManager.getInstance().sign(gradidoTransaction, [signingKeyPair])
const recipeTransactionController = await TransactionRecipe.create({
transaction: gradidoTransaction,
senderUser: transaction.senderUser,
recipientUser: transaction.recipientUser,
signingAccount,
recipientAccount,
backendTransactionId: transaction.backendTransactionId,
})
try {
await recipeTransactionController.getTransactionRecipeEntity().save()
ConditionalSleepManager.getInstance().signal(TRANSMIT_TO_IOTA_SLEEP_CONDITION_KEY)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
if (error.code === 'ER_DUP_ENTRY' && body.type === CrossGroupType.LOCAL) {
const existingRecipe = await findBySignature(
gradidoTransaction.sigMap.sigPair[0].signature,
)
if (!existingRecipe) {
throw new TransactionError(
TransactionErrorType.LOGIC_ERROR,
"recipe cannot be added because signature exist but couldn't load this existing receipt",
)
}
return new TransactionResult(new TransactionRecipeOutput(existingRecipe))
} else {
throw error
}
}
return new TransactionResult()
} catch (error) {
if (error instanceof TransactionError) {
return new TransactionResult(error)

View File

@ -0,0 +1,6 @@
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { Community } from '@entity/Community'
export abstract class CommunityRole {
abstract addCommunity(communityDraft: CommunityDraft, topic: string): Promise<Community>
}

View File

@ -0,0 +1,19 @@
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { Community } from '@entity/Community'
import { CommunityRole } from './Community.role'
import { logger } from '@/server/logger'
import { TransactionError } from '@/graphql/model/TransactionError'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { createCommunity } from '@/data/community.factory'
export class ForeignCommunityRole extends CommunityRole {
addCommunity(communityDraft: CommunityDraft, topic: string): Promise<Community> {
const community = createCommunity(communityDraft, topic)
try {
return community.save()
} catch (error) {
logger.error('error saving new foreign community into db: %s', error)
throw new TransactionError(TransactionErrorType.DB_ERROR, 'error saving community into db')
}
}
}

View File

@ -0,0 +1,28 @@
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { Community } from '@entity/Community'
import { CommunityRole } from './Community.role'
import { getTransaction } from '@/client/GradidoNode'
import { timestampSecondsToDate } from '@/utils/typeConverter'
import { createHomeCommunity } from '@/data/community.factory'
import { createCommunityRootTransactionRecipe } from '../transaction/transaction.context'
import { QueryRunner } from 'typeorm'
export class HomeCommunityRole extends CommunityRole {
public async addCommunity(communityDraft: CommunityDraft, topic: string): Promise<Community> {
const community = createHomeCommunity(communityDraft, topic)
// check if a CommunityRoot Transaction exist already on iota blockchain
const existingCommunityRootTransaction = await getTransaction(1, community.iotaTopic)
if (existingCommunityRootTransaction) {
community.confirmedAt = timestampSecondsToDate(existingCommunityRootTransaction.confirmedAt)
return community.save()
} else {
createCommunityRootTransactionRecipe(communityDraft, community).storeAsTransaction(
async (queryRunner: QueryRunner): Promise<void> => {
await queryRunner.manager.save(community)
},
)
}
return community.save()
}
}

View File

@ -0,0 +1,30 @@
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { TransactionResult } from '@/graphql/model/TransactionResult'
import { ForeignCommunityRole } from './ForeignCommunity.role'
import { HomeCommunityRole } from './HomeCommunity.role'
import { TransactionError } from '@/graphql/model/TransactionError'
import { TransactionsManager } from '@/controller/TransactionsManager'
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
export const addCommunity = async (
communityDraft: CommunityDraft,
iotaTopic?: string,
): Promise<TransactionResult> => {
const communityRole = communityDraft.foreign
? new ForeignCommunityRole()
: new HomeCommunityRole()
try {
if (!iotaTopic) {
iotaTopic = iotaTopicFromCommunityUUID(communityDraft.uuid)
}
await communityRole.addCommunity(communityDraft, iotaTopic)
await TransactionsManager.getInstance().addTopic(iotaTopic)
return new TransactionResult()
} catch (error) {
if (error instanceof TransactionError) {
return new TransactionResult(error)
} else {
throw error
}
}
}

View File

@ -0,0 +1,24 @@
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { TransactionRecipeRole } from './TransactionRecipe.role'
import { Community } from '@entity/Community'
import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder'
import { KeyPair } from '@/data/KeyPair'
import { sign } from '@/utils/cryptoHelper'
export class CommunityRootTransactionRole extends TransactionRecipeRole {
public createFromCommunityDraft(
communityDraft: CommunityDraft,
community: Community,
): CommunityRootTransactionRole {
// create proto transaction body
const transactionBody = new TransactionBodyBuilder()
.fromCommunityDraft(communityDraft, community)
.build()
// build transaction entity
this.transactionBuilder.fromTransactionBody(transactionBody).setSenderCommunity(community)
const transaction = this.transactionBuilder.getTransaction()
// sign
this.transactionBuilder.setSignature(sign(transaction.bodyBytes, new KeyPair(community)))
return this
}
}

View File

@ -0,0 +1,49 @@
import { TransactionBuilder } from '@/data/Transaction.builder'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { TransactionRecipe } from '@/graphql/model/TransactionRecipe'
import { TransactionResult } from '@/graphql/model/TransactionResult'
import { logger } from '@/server/logger'
import { getDataSource } from '@/typeorm/DataSource'
import { QueryRunner } from 'typeorm'
export class TransactionRecipeRole {
protected transactionBuilder: TransactionBuilder
construct() {
this.transactionBuilder = new TransactionBuilder()
}
public createFromTransactionDraft(transactionDraft: TransactionDraft): TransactionRecipeRole {
return this
}
public async storeAsTransaction(
transactionFunction: (queryRunner: QueryRunner) => Promise<void>,
): Promise<TransactionResult> {
const queryRunner = getDataSource().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction()
let result: TransactionResult
try {
const transactionRecipe = this.transactionBuilder.build()
await transactionFunction(queryRunner)
await queryRunner.manager.save(transactionRecipe)
await queryRunner.commitTransaction()
result = new TransactionResult(new TransactionRecipe(transactionRecipe))
} catch (err) {
logger.error('error saving new transaction recipe into db: %s', err)
result = new TransactionResult(
new TransactionError(
TransactionErrorType.DB_ERROR,
'error saving transaction recipe into db',
),
)
await queryRunner.rollbackTransaction()
} finally {
await queryRunner.release()
}
return result
}
}

View File

@ -0,0 +1,20 @@
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { Community } from '@entity/Community'
import { TransactionRecipeRole } from './TransactionRecipe.role'
import { CommunityRootTransactionRole } from './CommunityRootTransaction.role'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
export const createCommunityRootTransactionRecipe = (
communityDraft: CommunityDraft,
community: Community,
): TransactionRecipeRole => {
const communityRootTransactionRole = new CommunityRootTransactionRole()
return communityRootTransactionRole.createFromCommunityDraft(communityDraft, community)
}
export const createTransactionRecipe = (
transactionDraft: TransactionDraft,
): TransactionRecipeRole => {
const transactionRecipeRole = new TransactionRecipeRole()
return transactionRecipeRole.createFromTransactionDraft(transactionDraft)
}

View File

@ -1,32 +0,0 @@
import { Field, Message } from '@apollo/protobufjs'
import { TimestampSeconds } from './TimestampSeconds'
import { TransferAmount } from './TransferAmount'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
// 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> {
constructor(transaction: TransactionDraft) {
if (!transaction.targetDate) {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing targetDate for contribution',
)
}
super({
recipient: new TransferAmount({ amount: transaction.amount.toString() }),
targetDate: new TimestampSeconds(new Date(transaction.targetDate)),
})
}
@Field.d(1, TransferAmount)
public recipient: TransferAmount
@Field.d(3, 'TimestampSeconds')
public targetDate: TimestampSeconds
}

View File

@ -1,21 +0,0 @@
import { Field, Message } from '@apollo/protobufjs'
import { SignatureMap } from './SignatureMap'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class GradidoTransaction extends Message<GradidoTransaction> {
@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
}

View File

@ -1,23 +0,0 @@
import { Field, Message } from '@apollo/protobufjs'
import { TransferAmount } from './TransferAmount'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class GradidoTransfer extends Message<GradidoTransfer> {
constructor(transaction: TransactionDraft, coinOrigin?: string) {
super({
sender: new TransferAmount({
amount: transaction.amount.toString(),
communityId: coinOrigin,
}),
})
}
@Field.d(1, TransferAmount)
public sender: TransferAmount
@Field.d(2, 'bytes')
public recipient: Buffer
}

View File

@ -1,19 +0,0 @@
import { Field, Message } from '@apollo/protobufjs'
import { AddressType } from '@enum/AddressType'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class RegisterAddress extends Message<RegisterAddress> {
@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 subaccountPubkey: Buffer
}

View File

@ -1,66 +0,0 @@
import { Field, Message, OneOf } from '@apollo/protobufjs'
import { CrossGroupType } from '@/graphql/enum/CrossGroupType'
import { Timestamp } from './Timestamp'
import { GradidoTransfer } from './GradidoTransfer'
import { GradidoCreation } from './GradidoCreation'
import { GradidoDeferredTransfer } from './GradidoDeferredTransfer'
import { GroupFriendsUpdate } from './GroupFriendsUpdate'
import { RegisterAddress } from './RegisterAddress'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { determineCrossGroupType, determineOtherGroup } from '@/controller/TransactionBody'
// 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) {
const type = determineCrossGroupType(transaction)
super({
memo: 'Not implemented yet',
createdAt: new Timestamp(new Date(transaction.createdAt)),
versionNumber: '3.3',
type,
otherGroup: determineOtherGroup(type, 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',
)
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
}

View File

@ -0,0 +1,6 @@
import { KeyPair } from '@/data/KeyPair'
import { sign as ed25519Sign } from 'bip32-ed25519'
export const sign = (message: Buffer, keyPair: KeyPair): Buffer => {
return ed25519Sign(message, keyPair.getExtendPrivateKey())
}

View File

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

View File

@ -0,0 +1,17 @@
export const HARDENED_KEY_BITMASK = 0x80000000
/*
* change derivation index from x => x'
* for more infos to hardened keys look here:
* https://en.bitcoin.it/wiki/BIP_0032
*/
export const hardenDerivationIndex = (derivationIndex: number): number => {
/*
TypeScript uses signed integers by default,
but bip32-ed25519 expects an unsigned value for the derivation index.
The >>> shifts the bits 0 places to the right, which effectively makes no change to the value,
but forces TypeScript to treat derivationIndex as an unsigned value.
Source: ChatGPT
*/
return (derivationIndex | HARDENED_KEY_BITMASK) >>> 0
}

View File

@ -0,0 +1,12 @@
import 'reflect-metadata'
import { Timestamp } from '@/data/proto/3_3/Timestamp'
import { timestampToDate } from './typeConverter'
describe('utils/typeConverter', () => {
it('timestampToDate', () => {
const now = new Date('Thu, 05 Oct 2023 11:55:18 +0000')
const timestamp = new Timestamp(now)
expect(timestamp.seconds).toBe(Math.round(now.getTime() / 1000))
expect(timestampToDate(timestamp)).toEqual(now)
})
})

View File

@ -1,5 +1,12 @@
import { crypto_generichash as cryptoHash } from 'sodium-native'
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 { logger } from '@/server/logger'
import { TransactionError } from '@/graphql/model/TransactionError'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
export const uuid4ToBuffer = (uuid: string): Buffer => {
// Remove dashes from the UUIDv4 string
const cleanedUUID = uuid.replace(/-/g, '')
@ -15,3 +22,41 @@ export const iotaTopicFromCommunityUUID = (communityUUID: string): string => {
cryptoHash(hash, uuid4ToBuffer(communityUUID))
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',
)
}
}

View File

@ -55,8 +55,7 @@
"@resolver/*": ["src/graphql/resolver/*"],
"@scalar/*": ["src/graphql/scalar/*"],
"@test/*": ["test/*"],
"@proto/*" : ["src/proto/*"],
"@controller/*": ["src/controller/*"],
"@proto/*" : ["src/proto/*"],
"@validator/*" : ["src/graphql/validator/*"],
"@typeorm/*" : ["src/typeorm/*"],
/* external */

View File

@ -20,7 +20,7 @@
resolved "https://registry.yarnpkg.com/@apollo/cache-control-types/-/cache-control-types-1.0.3.tgz#5da62cf64c3b4419dabfef4536b57a40c8ff0b47"
integrity sha512-F17/vCp7QVwom9eG7ToauIKdAxpSoadsJnqIfyryLFSkLSOEqu+eC5Z3N8OXcUVStuOMcNHlyraRsA6rRICu4g==
"@apollo/protobufjs@1.2.7", "@apollo/protobufjs@^1.2.7":
"@apollo/protobufjs@1.2.7":
version "1.2.7"
resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.2.7.tgz#3a8675512817e4a046a897e5f4f16415f16a7d8a"
integrity sha512-Lahx5zntHPZia35myYDBRuF58tlwPskwHc5CWBZC/4bMKB6siTBWwtMrkqXcsNwQiFSzSx5hKdRPUmemrEp3Gg==
@ -843,6 +843,11 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@noble/hashes@^1.2.0":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@ -1125,6 +1130,13 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.7.0.tgz#c03de4572f114a940bc2ca909a33ddb2b925e470"
integrity sha512-zI22/pJW2wUZOVyguFaUL1HABdmSVxpXrzIqkjsHmyUjNhPoWM1CKfvVuXfetHhIok4RY573cqS0mZ1SJEnoTg==
"@types/node@>=13.7.0":
version "20.8.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.7.tgz#ad23827850843de973096edfc5abc9e922492a25"
integrity sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==
dependencies:
undici-types "~5.25.1"
"@types/node@^18.11.18":
version "18.18.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.0.tgz#bd19d5133a6e5e2d0152ec079ac27c120e7f1763"
@ -1628,6 +1640,22 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
bip32-ed25519@^0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/bip32-ed25519/-/bip32-ed25519-0.0.4.tgz#218943e212c2d3152dfd6f3a929305e3fe86534c"
integrity sha512-KfazzGVLwl70WZ1r98dO+8yaJRTGgWHL9ITn4bXHQi2mB4cT3Hjh53tXWUpEWE1zKCln7PbyX8Z337VapAOb5w==
dependencies:
bn.js "^5.1.1"
elliptic "^6.4.1"
hash.js "^1.1.7"
bip39@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.1.0.tgz#c55a418deaf48826a6ceb34ac55b3ee1577e18a3"
integrity sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==
dependencies:
"@noble/hashes" "^1.2.0"
bl@^4.0.3:
version "4.1.0"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
@ -1637,6 +1665,16 @@ bl@^4.0.3:
inherits "^2.0.4"
readable-stream "^3.4.0"
bn.js@^4.11.9:
version "4.12.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
bn.js@^5.1.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70"
integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==
body-parser@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
@ -1711,6 +1749,11 @@ braces@^3.0.2, braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
brorand@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==
browser-process-hrtime@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
@ -2343,6 +2386,19 @@ electron-to-chromium@^1.4.530:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.531.tgz#22966d894c4680726c17cf2908ee82ff5d26ac25"
integrity sha512-H6gi5E41Rn3/mhKlPaT1aIMg/71hTAqn0gYEllSuw9igNWtvQwu185jiCZoZD29n7Zukgh7GVZ3zGf0XvkhqjQ==
elliptic@^6.4.1:
version "6.5.4"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
dependencies:
bn.js "^4.11.9"
brorand "^1.1.0"
hash.js "^1.0.0"
hmac-drbg "^1.0.1"
inherits "^2.0.4"
minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.1"
emittery@^0.8.1:
version "0.8.1"
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860"
@ -3338,11 +3394,28 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7:
version "1.1.7"
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
dependencies:
inherits "^2.0.3"
minimalistic-assert "^1.0.1"
highlight.js@^10.7.1:
version "10.7.3"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==
hmac-drbg@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==
dependencies:
hash.js "^1.0.3"
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
html-encoding-sniffer@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
@ -4379,6 +4452,11 @@ long@^4.0.0:
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
long@^5.0.0:
version "5.2.3"
resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1"
integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==
lower-case@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
@ -4494,6 +4572,16 @@ mimic-response@^2.0.0:
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43"
integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
minimalistic-crypto-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==
minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@ -5038,6 +5126,24 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
protobufjs@^7.2.5:
version "7.2.5"
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.5.tgz#45d5c57387a6d29a17aab6846dcc283f9b8e7f2d"
integrity sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==
dependencies:
"@protobufjs/aspromise" "^1.1.2"
"@protobufjs/base64" "^1.1.2"
"@protobufjs/codegen" "^2.0.4"
"@protobufjs/eventemitter" "^1.1.0"
"@protobufjs/fetch" "^1.1.0"
"@protobufjs/float" "^1.0.2"
"@protobufjs/inquire" "^1.1.0"
"@protobufjs/path" "^1.1.2"
"@protobufjs/pool" "^1.1.0"
"@protobufjs/utf8" "^1.1.0"
"@types/node" ">=13.7.0"
long "^5.0.0"
proxy-addr@~2.0.5, proxy-addr@~2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
@ -6202,6 +6308,11 @@ undefsafe@^2.0.5:
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==
undici-types@~5.25.1:
version "5.25.3"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.25.3.tgz#e044115914c85f0bcbb229f346ab739f064998c3"
integrity sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"

View File

@ -8,7 +8,7 @@ import {
BaseEntity,
} from 'typeorm'
import { User } from '../User'
import { TransactionRecipe } from '../TransactionRecipe'
import { TransactionRecipe } from './TransactionRecipe'
import { ConfirmedTransaction } from '../ConfirmedTransaction'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { Decimal } from 'decimal.js-light'

View File

@ -8,7 +8,7 @@ import {
BaseEntity,
} from 'typeorm'
import { Account } from '../Account'
import { TransactionRecipe } from '../TransactionRecipe'
import { TransactionRecipe } from './TransactionRecipe'
import { AccountCommunity } from '../AccountCommunity'
@Entity('communities')

View File

@ -10,7 +10,7 @@ import {
import { Decimal } from 'decimal.js-light'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { Account } from '../Account'
import { Account } from './Account'
import { TransactionRecipe } from '../TransactionRecipe'
@Entity('confirmed_transactions')

View File

@ -10,8 +10,8 @@ import {
import { Decimal } from 'decimal.js-light'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { Account } from '../Account'
import { Community } from '../Community'
import { Account } from './Account'
import { Community } from './Community'
import { ConfirmedTransaction } from '../ConfirmedTransaction'
@Entity('transaction_recipes')

View File

@ -10,7 +10,7 @@ import {
import { Decimal } from 'decimal.js-light'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { Account } from '../Account'
import { Account } from './Account'
import { TransactionRecipe } from '../TransactionRecipe'
@Entity('confirmed_transactions')

View File

@ -0,0 +1,92 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
OneToMany,
BaseEntity,
} from 'typeorm'
import { User } from '../User'
import { Transaction } from '../Transaction'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { Decimal } from 'decimal.js-light'
import { AccountCommunity } from '../AccountCommunity'
@Entity('accounts')
export class Account extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@ManyToOne(() => User, (user) => user.accounts, { cascade: true, eager: true }) // Assuming you have a User entity with 'accounts' relation
@JoinColumn({ name: 'user_id' })
user?: User
// if user id is null, account belongs to community gmw or auf
@Column({ name: 'user_id', type: 'int', unsigned: true, nullable: true })
userId?: number
@Column({ name: 'derivation_index', type: 'int', unsigned: true, nullable: true })
derivationIndex?: number
@Column({ name: 'derive2_pubkey', type: 'binary', length: 32, unique: true })
derive2Pubkey: Buffer
@Column({ type: 'tinyint', unsigned: true })
type: number
@Column({
name: 'created_at',
type: 'datetime',
precision: 3,
default: () => 'CURRENT_TIMESTAMP(3)',
})
createdAt: Date
@Column({ name: 'confirmed_at', type: 'datetime', nullable: true })
confirmedAt?: Date
@Column({
type: 'decimal',
precision: 40,
scale: 20,
default: 0,
transformer: DecimalTransformer,
})
balance: Decimal
@Column({
name: 'balance_date',
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP()',
})
balanceDate: Date
@Column({
name: 'balance_created_at',
type: 'decimal',
precision: 40,
scale: 20,
default: 0,
transformer: DecimalTransformer,
})
balanceCreatedAt: Decimal
@Column({
name: 'balance_created_at_date',
type: 'datetime',
precision: 3,
default: () => 'CURRENT_TIMESTAMP(3)',
})
balanceCreatedAtDate: Date
@OneToMany(() => AccountCommunity, (accountCommunity) => accountCommunity.account)
@JoinColumn({ name: 'account_id' })
accountCommunities: AccountCommunity[]
@OneToMany(() => Transaction, (transaction) => transaction.signingAccount)
transactionSigning?: Transaction[]
@OneToMany(() => Transaction, (transaction) => transaction.recipientAccount)
transactionRecipient?: Transaction[]
}

View File

@ -0,0 +1,68 @@
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: 64, nullable: true })
rootPrivkey?: 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,
default: () => 'CURRENT_TIMESTAMP(3)',
})
createdAt: Date
@Column({ name: 'confirmed_at', type: 'datetime', nullable: true })
confirmedAt?: Date
@OneToMany(() => AccountCommunity, (accountCommunity) => accountCommunity.community)
@JoinColumn({ name: 'community_id' })
accountCommunities: AccountCommunity[]
@OneToMany(() => Transaction, (recipe) => recipe.senderCommunity)
transactionSender?: Transaction[]
@OneToMany(() => Transaction, (recipe) => recipe.recipientCommunity)
transactionRecipient?: Transaction[]
}

View File

@ -0,0 +1,120 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
OneToOne,
JoinColumn,
BaseEntity,
} from 'typeorm'
import { Decimal } from 'decimal.js-light'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
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
@Column({ name: 'backend_transaction_id', type: 'bigint', unsigned: true, nullable: true })
backendTransactionId?: number
@OneToOne(() => Transaction)
// eslint-disable-next-line no-use-before-define
paringTransaction?: Transaction
@Column({ name: 'paring_transaction_id', type: 'bigint', unsigned: true, nullable: true })
paringTransactionId?: 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.transactionSender, {
eager: true,
})
@JoinColumn({ name: 'sender_community_id' })
senderCommunity: Community
@Column({ name: 'sender_community_id', type: 'int', unsigned: true })
senderCommunityId: number
@ManyToOne(() => Community, (community) => community.transactionRecipient)
@JoinColumn({ name: 'recipient_community_id' })
recipientCommunity?: Community
@Column({ name: 'recipient_community_id', type: 'int', unsigned: true, nullable: true })
recipientCommunityId?: number
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: true,
transformer: DecimalTransformer,
})
amount?: Decimal
@Column({
name: 'account_balance_created_at',
type: 'decimal',
precision: 40,
scale: 20,
transformer: DecimalTransformer,
})
accountBalanceCreatedAt: Decimal
@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' })
nr: number
@Column({ name: 'running_hash', type: 'binary', length: 48 })
runningHash: Buffer
@Column({
name: 'account_balance',
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
default: 0,
transformer: DecimalTransformer,
})
accountBalanceConfirmedAt: Decimal
@Column({ name: 'iota_milestone', type: 'bigint', nullable: true })
iotaMilestone?: number
@Column({ name: 'confirmed_at', type: 'datetime' })
confirmedAt: Date
}

View File

@ -1 +1 @@
export { Account } from './0002-refactor_add_community/Account'
export { Account } from './0003-refactor_transaction_recipe/Account'

View File

@ -1 +1 @@
export { Community } from './0002-refactor_add_community/Community'
export { Community } from './0003-refactor_transaction_recipe/Community'

View File

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

View File

@ -1,19 +1,17 @@
import { Account } from './Account'
import { AccountCommunity } from './AccountCommunity'
import { Community } from './Community'
import { ConfirmedTransaction } from './ConfirmedTransaction'
import { InvalidTransaction } from './InvalidTransaction'
import { Migration } from './Migration'
import { TransactionRecipe } from './TransactionRecipe'
import { Transaction } from './Transaction'
import { User } from './User'
export const entities = [
AccountCommunity,
Account,
Community,
ConfirmedTransaction,
InvalidTransaction,
Migration,
TransactionRecipe,
Transaction,
User,
]

View File

@ -0,0 +1,97 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// write upgrade logic as parameter of queryFn
await queryFn(`DROP TABLE \`transaction_recipes\`;`)
await queryFn(`DROP TABLE \`confirmed_transactions\`;`)
await queryFn(
`ALTER TABLE \`accounts\` MODIFY COLUMN \`derivation_index\` int(10) unsigned NULL DEFAULT NULL;`,
)
await queryFn(
`ALTER TABLE \`accounts\` ADD COLUMN \`account_balance_created_at\` decimal(40,20) NOT NULL DEFAULT 0 AFTER \`balance_date\`;`,
)
await queryFn(
`ALTER TABLE \`accounts\` ADD COLUMN \`balance_created_at_date\` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) AFTER \`account_balance_created_at\`;`,
)
await queryFn(
`CREATE TABLE \`transactions\` (
\`id\` bigint unsigned NOT NULL AUTO_INCREMENT,
\`iota_message_id\` varbinary(32) DEFAULT NULL,
\`backend_transaction_id\` bigint unsigned DEFAULT NULL,
\`paring_transaction_id\` bigint unsigned DEFAULT NULL,
\`signing_account_id\` int unsigned DEFAULT NULL,
\`recipient_account_id\` int unsigned DEFAULT NULL,
\`sender_community_id\` int unsigned NOT NULL,
\`recipient_community_id\` int unsigned DEFAULT NULL,
\`amount\` decimal(40, 20) DEFAULT NULL,
\`account_balance_created_at\` decimal(40, 20) NOT NULL,
\`type\` tinyint NOT NULL,
\`created_at\` datetime(3) NOT NULL,
\`body_bytes\` blob NOT NULL,
\`signature\` varbinary(64) NOT NULL,
\`protocol_version\` varchar(255) NOT NULL DEFAULT '1',
\`nr\` bigint NOT NULL,
\`running_hash\` varbinary(48) NOT NULL,
\`account_balance\` decimal(40, 20) NOT NULL DEFAULT 0.00000000000000000000,
\`iota_milestone\` bigint DEFAULT NULL,
\`confirmed_at\` datetime NOT NULL,
PRIMARY KEY (\`id\`),
UNIQUE KEY \`signature\` (\`signature\`),
FOREIGN KEY (\`signing_account_id\`) REFERENCES accounts(id),
FOREIGN KEY (\`recipient_account_id\`) REFERENCES accounts(id),
FOREIGN KEY (\`sender_community_id\`) REFERENCES communities(id),
FOREIGN KEY (\`recipient_community_id\`) REFERENCES communities(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`,
)
await queryFn(`ALTER TABLE \`communities\` ADD UNIQUE(\`iota_topic\`);`)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`
CREATE TABLE IF NOT EXISTS \`transaction_recipes\` (
\`id\` bigint unsigned NOT NULL AUTO_INCREMENT,
\`iota_message_id\` binary(32) DEFAULT NULL,
\`signing_account_id\` int(10) unsigned NOT NULL,
\`recipient_account_id\` int(10) unsigned DEFAULT NULL,
\`sender_community_id\` int(10) unsigned NOT NULL,
\`recipient_community_id\` int(10) unsigned DEFAULT NULL,
\`amount\` decimal(40,20) DEFAULT NULL,
\`type\` tinyint unsigned NOT NULL,
\`created_at\` datetime(3) NOT NULL,
\`body_bytes\` BLOB NOT NULL,
\`signature\` binary(64) NOT NULL,
\`protocol_version\` int(10) NOT NULL DEFAULT 1,
PRIMARY KEY (\`id\`),
FOREIGN KEY (\`signing_account_id\`) REFERENCES accounts(id),
FOREIGN KEY (\`recipient_account_id\`) REFERENCES accounts(id),
FOREIGN KEY (\`sender_community_id\`) REFERENCES communities(id),
FOREIGN KEY (\`recipient_community_id\`) REFERENCES communities(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`)
await queryFn(`
CREATE TABLE IF NOT EXISTS \`confirmed_transactions\` (
\`id\` bigint unsigned NOT NULL AUTO_INCREMENT,
\`transaction_recipe_id\` bigint unsigned NOT NULL,
\`nr\` bigint unsigned NOT NULL,
\`running_hash\` binary(48) NOT NULL,
\`account_id\` int(10) unsigned NOT NULL,
\`account_balance\` decimal(40,20) NOT NULL DEFAULT 0,
\`iota_milestone\` bigint NOT NULL,
\`confirmed_at\` datetime NOT NULL,
PRIMARY KEY (\`id\`),
FOREIGN KEY (\`transaction_recipe_id\`) REFERENCES transaction_recipes(id),
FOREIGN KEY (\`account_id\`) REFERENCES accounts(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`)
await queryFn(
`ALTER TABLE \`accounts\` MODIFY COLUMN \`derivation_index\` int(10) unsigned NOT NULL;`,
)
await queryFn(`ALTER TABLE \`accounts\` DROP COLUMN \`account_balance_created_at\`;`)
await queryFn(`ALTER TABLE \`accounts\` DROP COLUMN \`balance_created_at_date\`;`)
await queryFn(`DROP TABLE \`transactions\`;`)
}