add new send to iota interaction

This commit is contained in:
einhornimmond 2024-09-22 16:35:15 +02:00
parent 68cb7b368b
commit 2727b6ebe9
31 changed files with 779 additions and 68 deletions

View File

@ -21,10 +21,12 @@ TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
# DLT-Connector
DLT_CONNECTOR_PORT=6010
# Gradido Node Server URL
NODE_SERVER_URL=http://localhost:8340
# Gradido Blockchain
GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET=21ffbbc616fe
GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
GRADIDO_BLOCKCHAIN_PRIVATE_KEY_ENCRYPTION_PASSWORD=YourPassword
# Route to Backend
BACKEND_SERVER_URL=http://localhost:4000

View File

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

View File

@ -33,6 +33,7 @@
"graphql-scalars": "^1.22.2",
"helmet": "^7.1.0",
"jose": "^5.2.2",
"jsonrpc-ts-client": "^0.2.3",
"log4js": "^6.7.1",
"nodemon": "^2.0.20",
"reflect-metadata": "^0.1.13",

View File

@ -0,0 +1,124 @@
/* eslint-disable camelcase */
import { AddressType, ConfirmedTransaction, stringToAddressType } from 'gradido-blockchain-js'
import JsonRpcClient from 'jsonrpc-ts-client'
import { JsonRpcEitherResponse } from 'jsonrpc-ts-client/dist/types/utils/jsonrpc'
import { CONFIG } from '@/config'
import { logger } from '@/logging/logger'
import { LogError } from '@/server/LogError'
import { confirmedTransactionFromBase64, getEnumValue } from '@/utils/typeConverter'
const client = new JsonRpcClient({
url: CONFIG.NODE_SERVER_URL,
})
/*
enum JsonRPCErrorCodes {
NONE = 0,
GRADIDO_NODE_ERROR = -10000,
UNKNOWN_GROUP = -10001,
NOT_IMPLEMENTED = -10002,
TRANSACTION_NOT_FOUND = -10003,
// default errors from json rpc standard: https://www.jsonrpc.org/specification
// -32700 Parse error Invalid JSON was received by the server.
PARSE_ERROR = -32700,
// -32600 Invalid Request The JSON sent is not a valid Request object.
INVALID_REQUEST = -32600,
// -32601 Method not found The method does not exist / is not available.
METHODE_NOT_FOUND = -32601,
// -32602 Invalid params Invalid method parameter(s).
INVALID_PARAMS = -32602,
// -32603 Internal error Internal JSON - RPC error.
INTERNAL_ERROR = -32603,
// -32000 to -32099 Server error Reserved for implementation-defined server-errors.
}
*/
interface ConfirmedTransactionList {
transactions: string[]
timeUsed: string
}
interface ConfirmedTransactionResponse {
transaction: string
timeUsed: string
}
interface AddressTypeResult {
addressType: string
}
function resolveResponse<T, R>(response: JsonRpcEitherResponse<T>, onSuccess: (result: T) => R): R {
if (response.isSuccess()) {
return onSuccess(response.result)
} else if (response.isError()) {
throw new LogError('error by json rpc request to gradido node server', response.error)
}
throw new LogError('no success and no error', response)
}
async function getTransactions(
fromTransactionId: number,
maxResultCount: number,
iotaTopic: string,
): Promise<ConfirmedTransaction[]> {
const parameter = {
format: 'base64',
fromTransactionId,
maxResultCount,
communityId: iotaTopic,
}
logger.info('call getTransactions on Node Server via jsonrpc 2.0 with ', parameter)
const response = await client.exec<ConfirmedTransactionList>('getTransactions', parameter) // sends payload {jsonrpc: '2.0', params: ...}
return resolveResponse(response, (result: ConfirmedTransactionList) => {
logger.debug('GradidoNode used time', result.timeUsed)
return result.transactions.map((transactionBase64) =>
confirmedTransactionFromBase64(transactionBase64),
)
})
}
async function getTransaction(
transactionId: number | Buffer,
iotaTopic: string,
): Promise<ConfirmedTransaction | undefined> {
logger.info('call gettransaction on Node Server via jsonrpc 2.0')
const response = await client.exec<ConfirmedTransactionResponse>('gettransaction', {
format: 'base64',
communityId: iotaTopic,
transactionId: typeof transactionId === 'number' ? transactionId : undefined,
iotaMessageId: transactionId instanceof Buffer ? transactionId.toString('hex') : undefined,
})
return resolveResponse(response, (result: ConfirmedTransactionResponse) => {
logger.debug('GradidoNode used time', result.timeUsed)
return result.transaction && result.transaction !== ''
? confirmedTransactionFromBase64(result.transaction)
: undefined
})
}
async function getLastTransaction(iotaTopic: string): Promise<ConfirmedTransaction | undefined> {
logger.info('call getlasttransaction on Node Server via jsonrpc 2.0')
const response = await client.exec<ConfirmedTransactionResponse>('getlasttransaction', {
format: 'base64',
communityId: iotaTopic,
})
return resolveResponse(response, (result: ConfirmedTransactionResponse) => {
logger.debug('GradidoNode used time', result.timeUsed)
return result.transaction && result.transaction !== ''
? confirmedTransactionFromBase64(result.transaction)
: undefined
})
}
async function getAddressType(pubkey: Buffer, iotaTopic: string): Promise<AddressType | undefined> {
logger.info('call getaddresstype on Node Server via jsonrpc 2.0')
const response = await client.exec<AddressTypeResult>('getaddresstype', {
pubkey: pubkey.toString('hex'),
communityId: iotaTopic,
})
return resolveResponse(response, (result: AddressTypeResult) =>
stringToAddressType(result.addressType),
)
}
export { getTransaction, getLastTransaction, getTransactions, getAddressType }

View File

@ -39,13 +39,15 @@ const dltConnector = {
DLT_CONNECTOR_PORT: process.env.DLT_CONNECTOR_PORT ?? 6010,
}
const nodeServer = {
NODE_SERVER_URL: process.env.NODE_SERVER_URL ?? 'http://localhost:8340',
}
const gradidoBlockchain = {
GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET:
process.env.GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET ?? 'invalid',
GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY:
process.env.GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY ?? 'invalid',
GRADIDO_BLOCKCHAIN_PRIVATE_KEY_ENCRYPTION_PASSWORD:
process.env.GRADIDO_BLOCKCHAIN_PRIVATE_KEY_ENCRYPTION_PASSWORD,
}
const backendServer = {
@ -70,6 +72,7 @@ export const CONFIG = {
...database,
...iota,
...dltConnector,
...nodeServer,
...gradidoBlockchain,
...backendServer,
}

View File

@ -8,7 +8,7 @@ import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { TransactionError } from '@/graphql/model/TransactionError'
import { LogError } from '@/server/LogError'
import { getDataSource } from '@/typeorm/DataSource'
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
import { uuid4ToHash } from '@/utils/typeConverter'
import { KeyPair } from './KeyPair'
@ -17,7 +17,7 @@ export const CommunityRepository = getDataSource()
.extend({
async isExist(community: CommunityDraft | string): Promise<boolean> {
const iotaTopic =
community instanceof CommunityDraft ? iotaTopicFromCommunityUUID(community.uuid) : community
community instanceof CommunityDraft ? uuid4ToHash(community.uuid) : community
const result = await this.find({
where: { iotaTopic },
})
@ -27,7 +27,7 @@ export const CommunityRepository = getDataSource()
async findByCommunityArg({ uuid, foreign, confirmed }: CommunityArg): Promise<Community[]> {
return await this.find({
where: {
...(uuid && { iotaTopic: iotaTopicFromCommunityUUID(uuid) }),
...(uuid && { iotaTopic: uuid4ToHash(uuid) }),
...(foreign && { foreign }),
...(confirmed && { confirmedAt: Not(IsNull()) }),
},
@ -35,7 +35,7 @@ export const CommunityRepository = getDataSource()
},
async findByCommunityUuid(communityUuid: string): Promise<Community | null> {
return await this.findOneBy({ iotaTopic: iotaTopicFromCommunityUUID(communityUuid) })
return await this.findOneBy({ iotaTopic: uuid4ToHash(communityUuid) })
},
async findByIotaTopic(iotaTopic: string): Promise<Community | null> {
@ -54,7 +54,7 @@ export const CommunityRepository = getDataSource()
}
return (
(await this.findOneBy({
iotaTopic: iotaTopicFromCommunityUUID(identifier.communityUuid),
iotaTopic: uuid4ToHash(identifier.communityUuid),
})) ?? undefined
)
},

View File

@ -1,26 +1,20 @@
import { Transaction } from '@entity/Transaction'
import { Field, Int, ObjectType } from 'type-graphql'
import { GradidoTransaction, MemoryBlock, transactionTypeToString } from 'gradido-blockchain-js'
import { Field, ObjectType } from 'type-graphql'
import { TransactionType } from '@/data/proto/3_3/enum/TransactionType'
import { LogError } from '@/server/LogError'
import { getEnumValue } from '@/utils/typeConverter'
@ObjectType()
export class TransactionRecipe {
public constructor({ id, createdAt, type, community, signature }: Transaction) {
const transactionType = getEnumValue(TransactionType, type)
if (!transactionType) {
throw new LogError('invalid transaction, type is missing')
public constructor(transaction: GradidoTransaction, messageId: MemoryBlock) {
const body = transaction.getTransactionBody()
if (!body) {
throw new LogError('invalid gradido transaction, cannot geht valid TransactionBody')
}
this.id = id
this.createdAt = createdAt.toString()
this.type = transactionType.toString()
this.topic = community.iotaTopic
this.signatureHex = signature.toString('hex')
}
@Field(() => Int)
id: number
this.createdAt = body.getCreatedAt().getDate().toString()
this.type = transactionTypeToString(body?.getTransactionType())
this.messageIdHex = messageId.convertToHex()
}
@Field(() => String)
createdAt: string
@ -29,8 +23,5 @@ export class TransactionRecipe {
type: string
@Field(() => String)
topic: string
@Field(() => String)
signatureHex: string
messageIdHex: string
}

View File

@ -10,7 +10,7 @@ import { CommunityRepository } from '@/data/Community.repository'
import { AddCommunityContext } from '@/interactions/backendToDb/community/AddCommunity.context'
import { logger } from '@/logging/logger'
import { LogError } from '@/server/LogError'
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
import { uuid4ToHash } from '@/utils/typeConverter'
@Resolver()
export class CommunityResolver {
@ -46,7 +46,7 @@ export class CommunityResolver {
communityDraft: CommunityDraft,
): Promise<TransactionResult> {
logger.info('addCommunity', communityDraft)
const topic = iotaTopicFromCommunityUUID(communityDraft.uuid)
const topic = uuid4ToHash(communityDraft.uuid)
// check if community was already written to db
if (await CommunityRepository.isExist(topic)) {
return new TransactionResult(

View File

@ -6,10 +6,10 @@ import { loadCryptoKeys, MemoryBlock } from 'gradido-blockchain-js'
import { CONFIG } from '@/config'
import { BackendClient } from './client/BackendClient'
import { CommunityRepository } from './data/Community.repository'
import { CommunityDraft } from './graphql/input/CommunityDraft'
import { AddCommunityContext } from './interactions/backendToDb/community/AddCommunity.context'
import { logger } from './logging/logger'
import { KeyPairCacheManager } from './manager/KeyPairCacheManager'
import createServer from './server/createServer'
import { LogError } from './server/LogError'
import { stopTransmitToIota, transmitToIota } from './tasks/transmitToIota'
@ -61,20 +61,17 @@ async function main() {
const { app } = await createServer()
// ask backend for home community if we haven't one
try {
await CommunityRepository.loadHomeCommunityKeyPair()
} catch (e) {
const backend = BackendClient.getInstance()
if (!backend) {
throw new LogError('cannot create backend client')
}
// wait for backend server to be ready
await waitForServer(backend, 2500, 10)
const communityDraft = await backend.getHomeCommunityDraft()
const addCommunityContext = new AddCommunityContext(communityDraft)
await addCommunityContext.run()
const backend = BackendClient.getInstance()
if (!backend) {
throw new LogError('cannot create backend client')
}
// wait for backend server to be ready
await waitForServer(backend, 2500, 10)
const communityDraft = await backend.getHomeCommunityDraft()
KeyPairCacheManager.getInstance().setHomeCommunityUUID(communityDraft.uuid)
const addCommunityContext = new AddCommunityContext(communityDraft)
await addCommunityContext.run()
// loop run all the time, check for new transaction for sending to iota
void transmitToIota()

View File

@ -1,5 +1,5 @@
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
import { uuid4ToHash } from '@/utils/typeConverter'
import { CommunityRole } from './Community.role'
import { ForeignCommunityRole } from './ForeignCommunity.role'
@ -15,7 +15,7 @@ export class AddCommunityContext {
private iotaTopic: string
public constructor(private communityDraft: CommunityDraft, iotaTopic?: string) {
if (!iotaTopic) {
this.iotaTopic = iotaTopicFromCommunityUUID(this.communityDraft.uuid)
this.iotaTopic = uuid4ToHash(this.communityDraft.uuid)
} else {
this.iotaTopic = iotaTopic
}

View File

@ -22,7 +22,7 @@ import { AccountType } from '@/graphql/enum/AccountType'
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
import { uuid4ToHash } from '@/utils/typeConverter'
import { CreateTransactionRecipeContext } from './CreateTransactionRecipe.context'
@ -50,8 +50,8 @@ let secondUser: UserSet
let foreignUser: UserSet
let homeCommunity: Community
const topic = iotaTopicFromCommunityUUID(homeCommunityUuid)
const foreignTopic = iotaTopicFromCommunityUUID(foreignCommunityUuid)
const topic = uuid4ToHash(homeCommunityUuid)
const foreignTopic = uuid4ToHash(foreignCommunityUuid)
describe('interactions/backendToDb/transaction/Create Transaction Recipe Context Test', () => {
beforeAll(async () => {

View File

@ -0,0 +1,5 @@
import { KeyPairEd25519 } from 'gradido-blockchain-js'
export abstract class AbstractKeyPairRole {
public abstract generateKeyPair(): KeyPairEd25519
}

View File

@ -0,0 +1,5 @@
import { KeyPairEd25519 } from 'gradido-blockchain-js'
export abstract class AbstractRemoteKeyPairRole {
public abstract retrieveKeyPair(): Promise<KeyPairEd25519>
}

View File

@ -0,0 +1,13 @@
import { KeyPairEd25519 } from 'gradido-blockchain-js'
import { AbstractKeyPairRole } from './AbstractKeyPair.role'
export class AccountKeyPairRole extends AbstractKeyPairRole {
public constructor(private accountNr: number, private userKeyPair: KeyPairEd25519) {
super()
}
public generateKeyPair(): KeyPairEd25519 {
return this.userKeyPair.deriveChild(this.accountNr)
}
}

View File

@ -0,0 +1,31 @@
import { KeyPairEd25519 } from 'gradido-blockchain-js'
import { getTransaction } from '@/client/GradidoNode'
import { LogError } from '@/server/LogError'
import { uuid4ToHash } from '@/utils/typeConverter'
import { AbstractRemoteKeyPairRole } from './AbstractRemoteKeyPair.role'
export class ForeignCommunityKeyPairRole extends AbstractRemoteKeyPairRole {
public constructor(private communityUuid: string) {
super()
}
public async retrieveKeyPair(): Promise<KeyPairEd25519> {
const firstTransaction = await getTransaction(1, uuid4ToHash(this.communityUuid).convertToHex())
if (!firstTransaction) {
throw new LogError(
"GradidoNode Server don't know this community with uuid " + this.communityUuid,
)
}
const transactionBody = firstTransaction.getGradidoTransaction()?.getTransactionBody()
if (!transactionBody || !transactionBody.isCommunityRoot()) {
throw new LogError('get invalid confirmed transaction from gradido node')
}
const communityRoot = transactionBody.getCommunityRoot()
if (!communityRoot) {
throw new LogError('invalid confirmed transaction')
}
return new KeyPairEd25519(communityRoot.getPubkey())
}
}

View File

@ -0,0 +1,22 @@
import { KeyPairEd25519, MemoryBlock } from 'gradido-blockchain-js'
import { CONFIG } from '@/config'
import { LogError } from '@/server/LogError'
import { AbstractKeyPairRole } from './AbstractKeyPair.role'
export class HomeCommunityKeyPairRole extends AbstractKeyPairRole {
public generateKeyPair(): KeyPairEd25519 {
if (!CONFIG.IOTA_HOME_COMMUNITY_SEED) {
throw new LogError(
'IOTA_HOME_COMMUNITY_SEED is missing either in config or as environment variable',
)
}
const seed = MemoryBlock.fromHex(CONFIG.IOTA_HOME_COMMUNITY_SEED)
const keyPair = KeyPairEd25519.create(seed)
if (!keyPair) {
throw new LogError("couldn't create keyPair from IOTA_HOME_COMMUNITY_SEED")
}
return keyPair
}
}

View File

@ -0,0 +1,56 @@
import { KeyPairEd25519 } from 'gradido-blockchain-js'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { KeyPairCacheManager } from '@/manager/KeyPairCacheManager'
import { AbstractRemoteKeyPairRole } from './AbstractRemoteKeyPair.role'
import { AccountKeyPairRole } from './AccountKeyPair.role'
import { ForeignCommunityKeyPairRole } from './ForeignCommunityKeyPair.role'
import { HomeCommunityKeyPairRole } from './HomeCommunityKeyPair.role'
import { RemoteAccountKeyPairRole } from './RemoteAccountKeyPair.role'
import { UserKeyPairRole } from './UserKeyPair.role'
/**
* @DCI-Context
* Context for calculating key pair for signing transactions
*/
export async function KeyPairCalculation(input: UserIdentifier | string): Promise<KeyPairEd25519> {
const cache = KeyPairCacheManager.getInstance()
const keyPair = cache.findKeyPair(input)
if (keyPair) {
return keyPair
}
let communityUUID: string
if (input instanceof UserIdentifier) {
communityUUID = input.communityUuid
} else {
communityUUID = input
}
if (cache.getHomeCommunityUUID() !== communityUUID) {
// it isn't home community so we can only retrieve public keys
let role: AbstractRemoteKeyPairRole
if (input instanceof UserIdentifier) {
role = new RemoteAccountKeyPairRole(input)
} else {
role = new ForeignCommunityKeyPairRole(input)
}
const keyPair = await role.retrieveKeyPair()
cache.addKeyPair(input, keyPair)
return keyPair
}
let communityKeyPair = cache.findKeyPair(communityUUID)
if (!communityKeyPair) {
communityKeyPair = new HomeCommunityKeyPairRole().generateKeyPair()
cache.addKeyPair(communityUUID, communityKeyPair)
}
if (input instanceof UserIdentifier) {
const userKeyPair = new UserKeyPairRole(input, communityKeyPair).generateKeyPair()
const accountNr = input.accountNr ?? 1
const accountKeyPair = new AccountKeyPairRole(accountNr, userKeyPair).generateKeyPair()
cache.addKeyPair(input, accountKeyPair)
return accountKeyPair
}
return communityKeyPair
}

View File

@ -0,0 +1,35 @@
import { KeyPairEd25519 } from 'gradido-blockchain-js'
import { getTransactions } from '@/client/GradidoNode'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { LogError } from '@/server/LogError'
import { uuid4ToHash } from '@/utils/typeConverter'
import { AbstractRemoteKeyPairRole } from './AbstractRemoteKeyPair.role'
export class RemoteAccountKeyPairRole extends AbstractRemoteKeyPairRole {
public constructor(private user: UserIdentifier) {
super()
}
public async retrieveKeyPair(): Promise<KeyPairEd25519> {
const nameHash = uuid4ToHash(this.user.uuid)
const confirmedTransactions = await getTransactions(
0,
30,
uuid4ToHash(this.user.communityUuid).convertToHex(),
)
for (let i = 0; i < confirmedTransactions.length; i++) {
const transactionBody = confirmedTransactions[i].getGradidoTransaction()?.getTransactionBody()
if (transactionBody && transactionBody.isRegisterAddress()) {
const registerAddress = transactionBody.getRegisterAddress()
if (registerAddress && registerAddress.getNameHash()?.equal(nameHash)) {
return new KeyPairEd25519(registerAddress.getAccountPublicKey())
}
}
}
throw new LogError(
'cannot find remote user in first 30 transaction from remote blockchain, please wait for better recover implementation',
)
}
}

View File

@ -0,0 +1,28 @@
import { KeyPairEd25519 } from 'gradido-blockchain-js'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { hardenDerivationIndex } from '@/utils/derivationHelper'
import { uuid4ToBuffer } from '@/utils/typeConverter'
import { AbstractKeyPairRole } from './AbstractKeyPair.role'
export class UserKeyPairRole extends AbstractKeyPairRole {
public constructor(private user: UserIdentifier, private communityKeys: KeyPairEd25519) {
super()
}
public generateKeyPair(): KeyPairEd25519 {
// example gradido id: 03857ac1-9cc2-483e-8a91-e5b10f5b8d16 =>
// wholeHex: '03857ac19cc2483e8a91e5b10f5b8d16']
const wholeHex = uuid4ToBuffer(this.user.uuid)
const parts = []
for (let i = 0; i < 4; i++) {
parts[i] = hardenDerivationIndex(wholeHex.subarray(i * 4, (i + 1) * 4).readUInt32BE())
}
// parts: [2206563009, 2629978174, 2324817329, 2405141782]
return parts.reduce(
(keyPair: KeyPairEd25519, node: number) => keyPair.deriveChild(node),
this.communityKeys,
)
}
}

View File

@ -0,0 +1,7 @@
import { GradidoTransactionBuilder } from 'gradido-blockchain-js'
export abstract class AbstractTransactionRole {
abstract getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder>
abstract getSenderCommunityUuid(): string
abstract getRecipientCommunityUuid(): string
}

View File

@ -0,0 +1,47 @@
import { GradidoTransactionBuilder } from 'gradido-blockchain-js'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { LogError } from '@/server/LogError'
import {
AUF_ACCOUNT_DERIVATION_INDEX,
GMW_ACCOUNT_DERIVATION_INDEX,
hardenDerivationIndex,
} from '@/utils/derivationHelper'
import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.context'
import { AbstractTransactionRole } from './AbstractTransaction.role'
export class CommunityRootTransactionRole extends AbstractTransactionRole {
constructor(private self: CommunityDraft) {
super()
}
getSenderCommunityUuid(): string {
return this.self.uuid
}
getRecipientCommunityUuid(): string {
throw new LogError('cannot be used as cross group transaction')
}
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
const builder = new GradidoTransactionBuilder()
const communityKeyPair = await KeyPairCalculation(this.self.uuid)
const gmwKeyPair = communityKeyPair.deriveChild(
hardenDerivationIndex(GMW_ACCOUNT_DERIVATION_INDEX),
)
const aufKeyPair = communityKeyPair.deriveChild(
hardenDerivationIndex(AUF_ACCOUNT_DERIVATION_INDEX),
)
builder
.setCreatedAt(new Date(this.self.createdAt))
.setCommunityRoot(
communityKeyPair.getPublicKey(),
gmwKeyPair.getPublicKey(),
aufKeyPair.getPublicKey(),
)
.sign(communityKeyPair)
return builder
}
}

View File

@ -0,0 +1,39 @@
import { GradidoTransactionBuilder, TransferAmount } from 'gradido-blockchain-js'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { LogError } from '@/server/LogError'
import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.context'
import { AbstractTransactionRole } from './AbstractTransaction.role'
export class CreationTransactionRole extends AbstractTransactionRole {
constructor(private self: TransactionDraft) {
super()
}
getSenderCommunityUuid(): string {
return this.self.user.communityUuid
}
getRecipientCommunityUuid(): string {
throw new LogError('cannot be used as cross group transaction')
}
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
const builder = new GradidoTransactionBuilder()
const recipientKeyPair = await KeyPairCalculation(this.self.user)
const signerKeyPair = await KeyPairCalculation(this.self.linkedUser)
if (!this.self.targetDate) {
throw new LogError('target date missing for creation transaction')
}
builder
.setCreatedAt(new Date(this.self.createdAt))
.setTransactionCreation(
new TransferAmount(recipientKeyPair.getPublicKey(), this.self.amount.toString()),
new Date(this.self.targetDate),
)
.sign(signerKeyPair)
return builder
}
}

View File

@ -0,0 +1,40 @@
/* eslint-disable camelcase */
import { AddressType_COMMUNITY_HUMAN, GradidoTransactionBuilder } from 'gradido-blockchain-js'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { LogError } from '@/server/LogError'
import { uuid4ToHash } from '@/utils/typeConverter'
import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.context'
import { AbstractTransactionRole } from './AbstractTransaction.role'
export class RegisterAddressTransactionRole extends AbstractTransactionRole {
constructor(private self: UserAccountDraft) {
super()
}
getSenderCommunityUuid(): string {
return this.self.user.communityUuid
}
getRecipientCommunityUuid(): string {
throw new LogError('cannot yet be used as cross group transaction')
}
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
const builder = new GradidoTransactionBuilder()
const communityKeyPair = await KeyPairCalculation(this.self.user.communityUuid)
const accountKeyPair = await KeyPairCalculation(this.self.user)
builder
.setCreatedAt(new Date(this.self.createdAt))
.setRegisterAddress(
accountKeyPair.getPublicKey(),
AddressType_COMMUNITY_HUMAN,
uuid4ToHash(this.self.user.uuid),
)
.sign(communityKeyPair)
.sign(accountKeyPair)
return builder
}
}

View File

@ -0,0 +1,116 @@
/* eslint-disable camelcase */
import {
GradidoTransaction,
InteractionSerialize,
InteractionValidate,
MemoryBlock,
ValidateType_SINGLE,
} from 'gradido-blockchain-js'
import { sendMessage as iotaSendMessage } from '@/client/IotaClient'
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { TransactionRecipe } from '@/graphql/model/TransactionRecipe'
import { TransactionResult } from '@/graphql/model/TransactionResult'
import { logger } from '@/logging/logger'
import { LogError } from '@/server/LogError'
import { uuid4ToHash } from '@/utils/typeConverter'
import { AbstractTransactionRole } from './AbstractTransaction.role'
import { CommunityRootTransactionRole } from './CommunityRootTransaction.role'
import { CreationTransactionRole } from './CreationTransaction.role'
import { RegisterAddressTransactionRole } from './RegisterAddressTransaction.role'
import { TransferTransactionRole } from './TransferTransaction.role'
/**
* @DCI-Context
* Context for sending transaction to iota
* send every transaction only once to iota!
*/
export async function SendToIotaContext(
input: TransactionDraft | UserAccountDraft | CommunityDraft,
): Promise<TransactionResult> {
const validate = (transaction: GradidoTransaction): void => {
try {
// throw an exception when something is wrong
const validator = new InteractionValidate(transaction)
validator.run(ValidateType_SINGLE)
} catch (e) {
if (e instanceof Error) {
throw new TransactionError(TransactionErrorType.VALIDATION_ERROR, e.message)
} else if (typeof e === 'string') {
throw new TransactionError(TransactionErrorType.VALIDATION_ERROR, e)
} else {
throw e
}
}
}
const sendViaIota = async (
gradidoTransaction: GradidoTransaction,
topic: string,
): Promise<MemoryBlock> => {
// protobuf serializing function
const serialized = new InteractionSerialize(gradidoTransaction).run()
if (!serialized) {
throw new TransactionError(
TransactionErrorType.PROTO_ENCODE_ERROR,
'cannot serialize transaction',
)
}
const resultMessage = await iotaSendMessage(
Uint8Array.from(serialized.data()),
Uint8Array.from(Buffer.from(topic, 'hex')),
)
logger.info('transmitted Gradido Transaction to Iota', {
messageId: resultMessage.messageId,
})
return MemoryBlock.fromHex(resultMessage.messageId)
}
let role: AbstractTransactionRole
if (input instanceof TransactionDraft) {
if (input.type === InputTransactionType.CREATION) {
role = new CreationTransactionRole(input)
} else if (input.type === InputTransactionType.SEND) {
role = new TransferTransactionRole(input)
} else {
throw new LogError('not supported transaction type')
}
} else if (input instanceof UserAccountDraft) {
role = new RegisterAddressTransactionRole(input)
} else if (input instanceof CommunityDraft) {
role = new CommunityRootTransactionRole(input)
} else {
throw new LogError('not expected input')
}
const builder = await role.getGradidoTransactionBuilder()
if (builder.isCrossCommunityTransaction()) {
const outboundTransaction = builder.buildOutbound()
validate(outboundTransaction)
const outboundIotaMessageId = await sendViaIota(
outboundTransaction,
uuid4ToHash(role.getSenderCommunityUuid()).convertToHex(),
)
builder.setParentMessageId(outboundIotaMessageId)
const inboundTransaction = builder.buildInbound()
validate(inboundTransaction)
await sendViaIota(
inboundTransaction,
uuid4ToHash(role.getRecipientCommunityUuid()).convertToHex(),
)
return new TransactionResult(new TransactionRecipe(outboundTransaction, outboundIotaMessageId))
} else {
const transaction = builder.build()
validate(transaction)
const iotaMessageId = await sendViaIota(
transaction,
uuid4ToHash(role.getSenderCommunityUuid()).convertToHex(),
)
return new TransactionResult(new TransactionRecipe(transaction, iotaMessageId))
}
}

View File

@ -0,0 +1,44 @@
import { GradidoTransactionBuilder, TransferAmount } from 'gradido-blockchain-js'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { uuid4ToHash } from '@/utils/typeConverter'
import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.context'
import { AbstractTransactionRole } from './AbstractTransaction.role'
export class TransferTransactionRole extends AbstractTransactionRole {
constructor(private self: TransactionDraft) {
super()
}
getSenderCommunityUuid(): string {
return this.self.user.communityUuid
}
getRecipientCommunityUuid(): string {
return this.self.linkedUser.communityUuid
}
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
const builder = new GradidoTransactionBuilder()
const senderKeyPair = await KeyPairCalculation(this.self.user)
const recipientKeyPair = await KeyPairCalculation(this.self.linkedUser)
builder
.setCreatedAt(new Date(this.self.createdAt))
.setTransactionTransfer(
new TransferAmount(senderKeyPair.getPublicKey(), this.self.amount.toString()),
recipientKeyPair.getPublicKey(),
)
const senderCommunity = this.self.user.communityUuid
const recipientCommunity = this.self.linkedUser.communityUuid
if (senderCommunity !== recipientCommunity) {
// we have a cross group transaction
builder
.setSenderCommunity(uuid4ToHash(senderCommunity).convertToHex())
.setRecipientCommunity(uuid4ToHash(recipientCommunity).convertToHex())
}
builder.sign(senderKeyPair)
return builder
}
}

View File

@ -0,0 +1,66 @@
import { KeyPairEd25519 } from 'gradido-blockchain-js'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { LogError } from '@/server/LogError'
// 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 KeyPairCacheManager {
// eslint-disable-next-line no-use-before-define
private static instance: KeyPairCacheManager
private cache: Map<string, KeyPairEd25519> = new Map<string, KeyPairEd25519>()
private homeCommunityUUID: string
/**
* 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(): KeyPairCacheManager {
if (!KeyPairCacheManager.instance) {
KeyPairCacheManager.instance = new KeyPairCacheManager()
}
return KeyPairCacheManager.instance
}
public setHomeCommunityUUID(uuid: string): void {
this.homeCommunityUUID = uuid
}
public getHomeCommunityUUID(): string {
return this.homeCommunityUUID
}
public findKeyPair(input: UserIdentifier | string): KeyPairEd25519 | undefined {
return this.cache.get(this.getKey(input))
}
public addKeyPair(input: UserIdentifier | string, keyPair: KeyPairEd25519): void {
const key = this.getKey(input)
if (this.cache.has(key)) {
throw new LogError('key already exist, cannot add', key)
}
this.cache.set(key, keyPair)
}
protected getKey(input: UserIdentifier | string): string {
if (input instanceof UserIdentifier) {
return input.uuid
} else {
return input
}
}
}

View File

@ -1,4 +1,6 @@
export const HARDENED_KEY_BITMASK = 0x80000000
export const GMW_ACCOUNT_DERIVATION_INDEX = 1
export const AUF_ACCOUNT_DERIVATION_INDEX = 2
/*
* change derivation index from x => x'

View File

@ -1,6 +1,6 @@
import 'reflect-metadata'
import { base64ToBuffer, iotaTopicFromCommunityUUID, uuid4ToBuffer } from './typeConverter'
import { base64ToBuffer, uuid4ToHash, uuid4ToBuffer } from './typeConverter'
describe('utils/typeConverter', () => {
it('uuid4ToBuffer', () => {
@ -10,7 +10,7 @@ describe('utils/typeConverter', () => {
})
it('iotaTopicFromCommunityUUID', () => {
expect(iotaTopicFromCommunityUUID('4f28e081-5c39-4dde-b6a4-3bde71de8d65')).toBe(
expect(uuid4ToHash('4f28e081-5c39-4dde-b6a4-3bde71de8d65')).toBe(
'3138b3590311fdf0a823e173caa9487b7d275c23fab07106b4b1364cb038affd',
)
})

View File

@ -8,10 +8,14 @@ import {
AddressType_CRYPTO_ACCOUNT,
AddressType_NONE,
AddressType_SUBACCOUNT,
ConfirmedTransaction,
DeserializeType_CONFIRMED_TRANSACTION,
InteractionDeserialize,
MemoryBlock,
} from 'gradido-blockchain-js'
import { crypto_generichash as cryptoHash } from 'sodium-native'
import { AccountType } from '@/graphql/enum/AccountType'
import { LogError } from '@/server/LogError'
export const uuid4ToBuffer = (uuid: string): Buffer => {
// Remove dashes from the UUIDv4 string
@ -23,10 +27,13 @@ export const uuid4ToBuffer = (uuid: string): Buffer => {
return buffer
}
export const iotaTopicFromCommunityUUID = (communityUUID: string): string => {
const hash = Buffer.alloc(32)
cryptoHash(hash, uuid4ToBuffer(communityUUID))
return hash.toString('hex')
export const uuid4ToMemoryBlock = (uuid: string): MemoryBlock => {
// Remove dashes from the UUIDv4 string
return MemoryBlock.fromHex(uuid.replace(/-/g, ''))
}
export const uuid4ToHash = (communityUUID: string): MemoryBlock => {
return uuid4ToMemoryBlock(communityUUID).calculateHash()
}
export const base64ToBuffer = (base64: string): Buffer => {
@ -86,3 +93,16 @@ export const addressTypeToAccountType = (type: AddressType): AccountType => {
return AccountType.NONE
}
}
export const confirmedTransactionFromBase64 = (base64: string): ConfirmedTransaction => {
const deserializer = new InteractionDeserialize(
MemoryBlock.fromBase64(base64),
DeserializeType_CONFIRMED_TRANSACTION,
)
deserializer.run()
const confirmedTransaction = deserializer.getConfirmedTransaction()
if (!confirmedTransaction) {
throw new LogError("invalid data, couldn't deserialize")
}
return confirmedTransaction
}

View File

@ -3,7 +3,7 @@ import { Community } from '@entity/Community'
import { KeyPair } from '@/data/KeyPair'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { AddCommunityContext } from '@/interactions/backendToDb/community/AddCommunity.context'
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
import { uuid4ToHash } from '@/utils/typeConverter'
export const communitySeed = async (
uuid: string,
@ -14,7 +14,7 @@ export const communitySeed = async (
homeCommunityDraft.uuid = uuid
homeCommunityDraft.foreign = foreign
homeCommunityDraft.createdAt = new Date().toISOString()
const iotaTopic = iotaTopicFromCommunityUUID(uuid)
const iotaTopic = uuid4ToHash(uuid)
const addCommunityContext = new AddCommunityContext(homeCommunityDraft, iotaTopic)
await addCommunityContext.run()

View File

@ -1589,6 +1589,13 @@ available-typed-arrays@^1.0.7:
dependencies:
possible-typed-array-names "^1.0.0"
axios@^0.24.0:
version "0.24.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6"
integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==
dependencies:
follow-redirects "^1.14.4"
axios@^1.6.5:
version "1.7.7"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
@ -2254,7 +2261,7 @@ debug@2.6.9:
dependencies:
ms "2.0.0"
debug@4, debug@^4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5:
debug@4, debug@^4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.5:
version "4.3.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
@ -2463,9 +2470,9 @@ ee-first@1.1.1:
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
electron-to-chromium@^1.5.4:
version "1.5.26"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.26.tgz#449b4fa90e83ab98abbe3b6a96c8ee395de94452"
integrity sha512-Z+OMe9M/V6Ep9n/52+b7lkvYEps26z4Yz3vjWL1V61W0q+VLF1pOHhMY17sa4roz4AWmULSI8E6SAojZA5L0YQ==
version "1.5.27"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.27.tgz#5203ce5d6054857d84ba84d3681cbe59132ade78"
integrity sha512-o37j1vZqCoEgBuWWXLHQgTN/KDKe7zwpiY5CPeq2RvUqOyJw9xnrULzZAEVQ5p4h+zjMk7hgtOoPdnLxr7m/jw==
emittery@^0.8.1:
version "0.8.1"
@ -3182,7 +3189,7 @@ flatted@^3.2.7, flatted@^3.2.9:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a"
integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==
follow-redirects@^1.15.6:
follow-redirects@^1.14.4, follow-redirects@^1.15.6:
version "1.15.9"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
@ -3468,7 +3475,7 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9:
"gradido-blockchain-js@git+https://github.com/gradido/gradido-blockchain-js#master":
version "0.0.1"
resolved "git+https://github.com/gradido/gradido-blockchain-js#02aaeefc015c8ec8b1a2c453d75e7c2cf803a7c2"
resolved "git+https://github.com/gradido/gradido-blockchain-js#5e7bc50af82d30ef0fdbe48414b1f916c592b6f4"
dependencies:
bindings "^1.5.0"
nan "^2.20.0"
@ -4528,6 +4535,14 @@ jsonfile@^6.0.1:
optionalDependencies:
graceful-fs "^4.1.6"
jsonrpc-ts-client@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/jsonrpc-ts-client/-/jsonrpc-ts-client-0.2.3.tgz#ec50c413d84041564e6c8a4003ab4bb360d5cfcc"
integrity sha512-9uYpKrZKN3/3+9MYA/0vdhl9/esn59u6I9Qj6ohczxKwJ+e7DD4prf3i2nSdAl0Wlw5gBHZOL3wajSa1uiE16g==
dependencies:
axios "^0.24.0"
debug "^4.3.3"
keyv@^4.5.3:
version "4.5.4"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
@ -4554,9 +4569,9 @@ levn@^0.4.1:
type-check "~0.4.0"
libphonenumber-js@^1.10.53:
version "1.11.8"
resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.8.tgz#697fdd36500a97bc672d7927d867edf34b4bd2a7"
integrity sha512-0fv/YKpJBAgXKy0kaS3fnqoUVN8901vUYAKIGD/MWZaDfhJt1nZjPL3ZzdZBt/G8G8Hw2J1xOIrXWdNHFHPAvg==
version "1.11.9"
resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.9.tgz#e653042b11da2b50b7ea3b206fa7ca998436ae99"
integrity sha512-Zs5wf5HaWzW2/inlupe2tstl0I/Tbqo7lH20ZLr6Is58u7Dz2n+gRFGNlj9/gWxFvNfp9+YyDsiegjNhdixB9A==
lines-and-columns@^1.1.6:
version "1.2.4"