introduce valibot, elysia.js, trpc and drop apollo/graphql-server

This commit is contained in:
einhornimmond 2025-08-01 12:54:26 +02:00
parent bdc26c524b
commit 6e2269a499
27 changed files with 607 additions and 418 deletions

View File

@ -184,12 +184,15 @@
"dependencies": {
"database": "*",
"esbuild": "^0.25.2",
"jose": "^4.14.4",
"log4js": "^6.9.1",
"shared": "*",
"zod": "^3.25.61",
},
"devDependencies": {
"@biomejs/biome": "2.0.0",
"@types/node": "^17.0.21",
"type-graphql": "^1.1.1",
"typescript": "^4.9.5",
},
},
@ -295,6 +298,7 @@
"await-semaphore": "0.1.3",
"class-validator": "^0.13.2",
"config-schema": "*",
"core": "*",
"cors": "2.8.5",
"database": "*",
"decimal.js-light": "^2.5.1",
@ -423,6 +427,7 @@
"dependencies": {
"decimal.js-light": "^2.5.1",
"esbuild": "^0.25.2",
"jose": "^4.14.4",
"log4js": "^6.9.1",
"zod": "^3.25.61",
},

View File

@ -5,7 +5,7 @@
"name": "dlt-connector",
"dependencies": {
"@iota/client": "^2.2.4",
"gradido-blockchain-js": "git+https://github.com/gradido/gradido-blockchain-js#217d03b",
"gradido-blockchain-js": "git+https://github.com/gradido/gradido-blockchain-js#9a5f392",
},
"devDependencies": {
"@biomejs/biome": "2.0.0",
@ -232,7 +232,7 @@
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"gradido-blockchain-js": ["gradido-blockchain-js@github:gradido/gradido-blockchain-js#217d03b", { "dependencies": { "bindings": "^1.5.0", "nan": "^2.20.0", "node-addon-api": "^7.1.1", "node-gyp-build": "^4.8.1", "prebuildify": "git+https://github.com/einhornimmond/prebuildify#cmake_js" } }, "gradido-gradido-blockchain-js-217d03b"],
"gradido-blockchain-js": ["gradido-blockchain-js@github:gradido/gradido-blockchain-js#9a5f392", { "dependencies": { "bindings": "^1.5.0", "nan": "^2.20.0", "node-addon-api": "^7.1.1", "node-gyp-build": "^4.8.1", "prebuildify": "git+https://github.com/einhornimmond/prebuildify#65d94455fab86b902c0d59bb9c06ac70470e56b2" } }, "gradido-gradido-blockchain-js-9a5f392"],
"graphql": ["graphql@16.11.0", "", {}, "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw=="],
@ -356,7 +356,7 @@
"prebuild-install": ["prebuild-install@6.1.4", "", { "dependencies": { "detect-libc": "^1.0.3", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", "node-abi": "^2.21.0", "npmlog": "^4.0.1", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^3.0.3", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ=="],
"prebuildify": ["prebuildify@github:einhornimmond/prebuildify#91f4e76", { "dependencies": { "cmake-js": "^7.2.1", "execspawn": "^1.0.1", "minimist": "^1.2.5", "mkdirp-classic": "^0.5.3", "node-abi": "^3.3.0", "npm-run-path": "^3.1.0", "npm-which": "^3.0.1", "pump": "^3.0.0", "tar-fs": "^2.1.0" }, "bin": { "prebuildify": "./bin.js" } }, "einhornimmond-prebuildify-91f4e76"],
"prebuildify": ["prebuildify@github:einhornimmond/prebuildify#65d9445", { "dependencies": { "cmake-js": "^7.2.1", "execspawn": "^1.0.1", "minimist": "^1.2.5", "mkdirp-classic": "^0.5.3", "node-abi": "^3.3.0", "npm-run-path": "^3.1.0", "npm-which": "^3.0.1", "pump": "^3.0.0", "tar-fs": "^2.1.0" }, "bin": { "prebuildify": "./bin.js" } }, "einhornimmond-prebuildify-65d9445"],
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],

View File

@ -12,7 +12,7 @@
"lint:fix": "biome check --error-on-warnings . --write"
},
"dependencies": {
"gradido-blockchain-js": "git+https://github.com/gradido/gradido-blockchain-js#217d03b",
"gradido-blockchain-js": "git+https://github.com/gradido/gradido-blockchain-js#9a5f392",
"@iota/client": "^2.2.4"
},
"devDependencies": {

View File

@ -0,0 +1,167 @@
import { AddressType, ConfirmedTransaction } from 'gradido-blockchain-js'
import { getLogger } from 'log4js'
import { LOG4JS_BASE_CATEGORY } from '../../config/const'
import * as v from 'valibot'
import { addressTypeSchema, confirmedTransactionSchema } from '../../schemas/typeConverter.schema'
import { GradidoNodeErrorCodes } from '../../enum/GradidoNodeErrorCodes'
import { rpcCall, rpcCallResolved, GradidoNodeRequestError } from './jsonrpc'
import {
TransactionsRangeInput,
TransactionIdentifierInput,
transactionsRangeSchema,
transactionIdentifierSchema
} from './input.schema'
import { Uuidv4Hash } from '../../data/Uuidv4Hash'
import { Hex32, Hex32Input, hex32Schema } from '../../schemas/typeGuard.schema'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.client.GradidoNode`)
/**
* getTransactions
* get list of confirmed transactions from a specific community
* @param input fromTransactionId is the id of the first transaction to return
* @param input maxResultCount is the max number of transactions to return
* @param input topic is the community topic
* @returns list of confirmed transactions
* @throws GradidoNodeRequestError
* @example
* ```
* const transactions = await getTransactions({
* fromTransactionId: 1,
* maxResultCount: 100,
* topic: communityUuid,
* })
* ```
*/
async function getTransactions(input: TransactionsRangeInput): Promise<ConfirmedTransaction[]> {
const parameter = { ...v.parse(transactionsRangeSchema, input), format: 'base64' }
const result = await rpcCallResolved<{transactions: string[]}>('getTransactions', parameter)
return result.transactions.map((transactionBase64) =>
v.parse(confirmedTransactionSchema, transactionBase64),
)
}
/**
* getTransaction
* get a specific confirmed transaction from a specific community
* @param transactionIdentifier
* @returns the confirmed transaction or undefined if transaction is not found
* @throws GradidoNodeRequestError
*/
async function getTransaction(transactionIdentifier: TransactionIdentifierInput)
: Promise<ConfirmedTransaction | undefined> {
const parameter = {
...v.parse(transactionIdentifierSchema, transactionIdentifier),
format: 'base64',
}
const response = await rpcCall<{ transaction: string }>('gettransaction', parameter)
if (response.isSuccess()) {
return v.parse(confirmedTransactionSchema, response.result.transaction)
}
if (response.isError()) {
if (response.error.code === GradidoNodeErrorCodes.TRANSACTION_NOT_FOUND) {
return undefined
}
}
throw new GradidoNodeRequestError(response.error.message, response)
}
/**
* getLastTransaction
* get the last confirmed transaction from a specific community
* @param iotaTopic the community topic
* @returns the last confirmed transaction or undefined if blockchain for community is empty or not found
* @throws GradidoNodeRequestError
*/
async function getLastTransaction(iotaTopic: Uuidv4Hash): Promise<ConfirmedTransaction | undefined> {
const response = await rpcCall<{ transaction: string }>('getlasttransaction', {
format: 'base64',
topic: iotaTopic.getAsHexString(),
})
if (response.isSuccess()) {
return v.parse(confirmedTransactionSchema, response.result.transaction)
}
if (response.isError()) {
if (response.error.code === GradidoNodeErrorCodes.GRADIDO_NODE_ERROR) {
return undefined
}
throw new GradidoNodeRequestError(response.error.message, response)
}
}
/**
* getAddressType
* get the address type of a specific user
* can be used to check if user/account exists on blockchain
* look also for gmw, auf and deferred transfer accounts
* @param pubkey the public key of the user or account
* @param iotaTopic the community topic
* @returns the address type of the user/account or undefined
* @throws GradidoNodeRequestError
*/
async function getAddressType(pubkey: Hex32Input, iotaTopic: Uuidv4Hash): Promise<AddressType> {
const parameter = {
pubkey: v.parse(hex32Schema, pubkey),
communityId: iotaTopic.getAsHexString(),
}
const response = await rpcCallResolved<{ addressType: string }>('getaddresstype', parameter)
return v.parse(addressTypeSchema, response.addressType)
}
/**
* findUserByNameHash
* find a user by name hash
* @param nameHash the name hash of the user
* @param iotaTopic the community topic
* @returns the public key of the user as hex32 string or undefined if user is not found
* @throws GradidoNodeRequestError
*/
async function findUserByNameHash(nameHash: Uuidv4Hash, iotaTopic: Uuidv4Hash): Promise<Hex32 | undefined> {
const parameter = {
nameHash: nameHash.getAsHexString(),
communityId: iotaTopic.getAsHexString(),
}
const response = await rpcCall<{ pubkey: string; timeUsed: string }>('findUserByNameHash', parameter)
if(response.isSuccess()) {
logger.info(`call findUserByNameHash, used ${response.result.timeUsed}`)
return v.parse(hex32Schema, response.result.pubkey)
}
if (response.isError() && response.error.code === GradidoNodeErrorCodes.JSON_RPC_ERROR_ADDRESS_NOT_FOUND) {
logger.debug(`call findUserByNameHash, return with error: ${response.error.message}`)
}
return undefined
}
/**
* getTransactionsForAccount
* get list of confirmed transactions for a specific account
* @param transactionRange the range of transactions to return
* @param pubkey the public key of the account
* @returns list of confirmed transactions
* @throws GradidoNodeRequestError
*/
async function getTransactionsForAccount(
transactionRange: TransactionsRangeInput,
pubkey: Hex32Input,
): Promise<ConfirmedTransaction[]> {
const parameter = {
...v.parse(transactionsRangeSchema, transactionRange),
pubkey: v.parse(hex32Schema, pubkey),
format: 'base64',
}
const response = await rpcCallResolved<{transactions: string[]}>('listtransactionsforaddress', parameter)
return response.transactions.map((transactionBase64) =>
v.parse(confirmedTransactionSchema, transactionBase64),
)
}
export {
getTransaction,
getLastTransaction,
getTransactions,
getAddressType,
getTransactionsForAccount,
findUserByNameHash,
}

View File

@ -1,33 +1,36 @@
import * as v from 'valibot'
import { uuid4ToTopicSchema } from '../../schemas/typeConverter.schema'
import { hex32Schema, iotaMessageIdSchema } from '../../schemas/typeGuard.schema'
export enum TransactionFormatType {
BASE64 = 'base64',
JSON = 'json',
}
export const transactionFormatTypeSchema = v.nullish(
v.enum(TransactionFormatType),
TransactionFormatType.BASE64
)
export type TransactionFormatTypeInput = v.InferInput<typeof transactionFormatTypeSchema>
export const getTransactionsInputSchema = v.object({
format: transactionFormatTypeSchema,
export const transactionsRangeSchema = v.object({
// default value is 1, from first transactions
fromTransactionId: v.undefinedable(v.pipe(v.number(), v.minValue(1, 'expect number >= 1')), 1),
// default value is 100, max 100 transactions
maxResultCount: v.undefinedable(v.pipe(v.number(), v.minValue(1, 'expect number >= 1')), 100),
communityId: uuid4ToTopicSchema,
topic: hex32Schema,
})
export type GetTransactionsInputType = v.InferInput<typeof getTransactionsInputSchema>
export type TransactionsRangeInput = v.InferInput<typeof transactionsRangeSchema>
export const getTransactionInputSchema = v.object({
transactionIdentifier: v.object({
iotaTopic: uuid4ToTopicSchema,
transactionNr: v.number(),
iotaMessageId: v.string(),
// allow TransactionIdentifier to only contain either transactionNr or iotaMessageId
export const transactionIdentifierSchema = v.pipe(
v.object({
transactionNr: v.nullish(
v.pipe(v.number('expect number type'), v.minValue(1, 'expect number >= 1')),
undefined
),
iotaMessageId: v.nullish(iotaMessageIdSchema, undefined),
topic: hex32Schema,
}),
})
v.custom((value: any) => {
const setFieldsCount = Number(value.transactionNr !== undefined) + Number(value.iotaMessageId !== undefined)
if (setFieldsCount !== 1) {
return false
}
return true
}, 'expect transactionNr or iotaMessageId not both')
)
export type TransactionIdentifierInput = v.InferInput<typeof transactionIdentifierSchema>
export type TransactionIdentifier = v.InferOutput<typeof transactionIdentifierSchema>

View File

@ -1,174 +0,0 @@
/* eslint-disable camelcase */
import { AddressType, ConfirmedTransaction, MemoryBlock, 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 { getLogger } from 'log4js'
import { LOG4JS_BASE_CATEGORY } from '../../config/const'
import * as v from 'valibot'
import { confirmedTransactionFromBase64Schema } from '../../schemas/typeConverter.schema'
import { isPortOpenRetry } from '../../utils/network'
import { TransactionIdentifierInput, transactionIdentifierSchema } from '../../schemas/transaction.schema'
import { GradidoNodeErrorCodes } from '../../enum/GradidoNodeErrorCodes'
import { GetTransactionsInputType, TransactionFormatTypeInput, getTransactionsInputSchema, transactionFormatTypeSchema } from './input.schema'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.client.GradidoNode`)
const client = new JsonRpcClient({
url: CONFIG.NODE_SERVER_URL,
})
interface ConfirmedTransactionList {
transactions: string[]
timeUsed: string
}
interface ConfirmedTransactionResponse {
transaction: string
timeUsed: string
}
interface AddressTypeResult {
addressType: string
}
interface FindUserResponse {
pubkey: string
timeUsed: string
}
export class GradidoNodeRequestError<T> extends Error {
private response?: JsonRpcEitherResponse<T>
constructor(message: string, response?: JsonRpcEitherResponse<T>) {
super(message)
this.name = 'GradidoNodeRequestError'
this.response = response
}
getResponse(): JsonRpcEitherResponse<T> | undefined {
return this.response
}
}
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 GradidoNodeRequestError(response.error.message, response)
}
throw new GradidoNodeRequestError('no success and no error')
}
async function getTransactions(input: GetTransactionsInputType): Promise<ConfirmedTransaction[]> {
const parameter = v.parse(getTransactionsInputSchema, input)
logger.debug('call getTransactions with ', parameter)
await isPortOpenRetry(CONFIG.NODE_SERVER_URL)
const response = await client.exec<ConfirmedTransactionList>('getTransactions', parameter)
return resolveResponse(response, (result: ConfirmedTransactionList) => {
logger.info(`call getTransactions, used ${result.timeUsed}`)
return result.transactions.map((transactionBase64) =>
v.parse(confirmedTransactionFromBase64Schema, transactionBase64),
)
})
}
async function getTransaction(
transactionIdentifier: TransactionIdentifierInput,
format: TransactionFormatTypeInput
)
: Promise<ConfirmedTransaction | undefined> {
const parameter = {
...v.parse(transactionIdentifierSchema, transactionIdentifier),
format: v.parse(transactionFormatTypeSchema, format),
}
logger.debug('call gettransaction with ', parameter)
await isPortOpenRetry(CONFIG.NODE_SERVER_URL)
const response = await client.exec<ConfirmedTransactionResponse>('gettransaction', parameter)
return resolveResponse(response, (result: ConfirmedTransactionResponse) => {
logger.info(`call gettransaction, used ${result.timeUsed}`)
return result.transaction && result.transaction !== ''
? v.parse(confirmedTransactionFromBase64Schema, result.transaction)
: undefined
})
}
async function getLastTransaction(iotaTopic: string): Promise<ConfirmedTransaction | undefined> {
const parameter = {
format: 'base64',
communityId: iotaTopic,
}
logger.debug('call getlasttransaction with ', parameter)
await isPortOpenRetry(CONFIG.NODE_SERVER_URL)
const response = await client.exec<ConfirmedTransactionResponse>('getlasttransaction', parameter)
return resolveResponse(response, (result: ConfirmedTransactionResponse) => {
logger.info(`call getlasttransaction, used ${result.timeUsed}`)
return result.transaction && result.transaction !== ''
? v.parse(confirmedTransactionFromBase64Schema, result.transaction)
: undefined
})
}
async function getAddressType(pubkey: Buffer, iotaTopic: string): Promise<AddressType | undefined> {
const parameter = {
pubkey: pubkey.toString('hex'),
communityId: iotaTopic,
}
logger.debug('call getaddresstype with ', parameter)
await isPortOpenRetry(CONFIG.NODE_SERVER_URL)
const response = await client.exec<AddressTypeResult>('getaddresstype', parameter)
return resolveResponse(response, (result: AddressTypeResult) => {
logger.info(`call getaddresstype`)
return stringToAddressType(result.addressType)
})
}
async function findUserByNameHash(nameHash: MemoryBlock, iotaTopic: string): Promise<MemoryBlock | undefined> {
const parameter = {
nameHash: nameHash.convertToHex(),
communityId: iotaTopic,
}
logger.debug('call findUserByNameHash with ', parameter)
await isPortOpenRetry(CONFIG.NODE_SERVER_URL)
const response = await client.exec<FindUserResponse>('findUserByNameHash', parameter)
if (response.isError() && response.error.code === GradidoNodeErrorCodes.JSON_RPC_ERROR_ADDRESS_NOT_FOUND) {
return undefined
}
return resolveResponse(response, (result: FindUserResponse) => {
logger.info(`call findUserByNameHash, used ${result.timeUsed}`)
return result.pubkey && result.pubkey !== '' ? MemoryBlock.fromHex(result.pubkey) : undefined
})
}
async function getTransactionsForAccount(
pubkey: MemoryBlock,
iotaTopic: string,
maxResultCount = 0,
firstTransactionNr = 1,
): Promise<ConfirmedTransaction[] | undefined> {
const parameter = {
pubkey: pubkey.convertToHex(),
format: 'base64',
firstTransactionNr,
maxResultCount,
communityId: iotaTopic,
}
logger.debug('call listtransactionsforaddress with ', parameter)
await isPortOpenRetry(CONFIG.NODE_SERVER_URL)
const response = await client.exec<ConfirmedTransactionList>('listtransactionsforaddress', parameter)
return resolveResponse(response, (result: ConfirmedTransactionList) => {
logger.info(`call listtransactionsforaddress, used ${result.timeUsed}`)
return result.transactions.map((transactionBase64) =>
v.parse(confirmedTransactionFromBase64Schema, transactionBase64),
)
})
}
export {
getTransaction,
getLastTransaction,
getTransactions,
getAddressType,
getTransactionsForAccount,
findUserByNameHash,
}

View File

@ -0,0 +1,54 @@
import { getLogger } from 'log4js'
import { LOG4JS_BASE_CATEGORY } from '../../config/const'
import { JsonRpcEitherResponse } from 'jsonrpc-ts-client/dist/types/utils/jsonrpc'
import { isPortOpenRetry } from '../../utils/network'
import { CONFIG } from '../../config'
import JsonRpcClient from 'jsonrpc-ts-client'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.client.GradidoNode`)
export const client = new JsonRpcClient({
url: CONFIG.NODE_SERVER_URL,
})
export class GradidoNodeRequestError<T> extends Error {
private response?: JsonRpcEitherResponse<T>
constructor(message: string, response?: JsonRpcEitherResponse<T>) {
super(message)
this.name = 'GradidoNodeRequestError'
this.response = response
}
getResponse(): JsonRpcEitherResponse<T> | undefined {
return this.response
}
}
// return result on success or throw error
export 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 GradidoNodeRequestError(response.error.message, response)
}
throw new GradidoNodeRequestError('no success and no error')
}
type WithTimeUsed<T> = T & { timeUsed?: string }
// template rpcCall, check first if port is open before executing json rpc 2.0 request
export async function rpcCall<T>(method: string, parameter: any): Promise<JsonRpcEitherResponse<T>> {
logger.debug('call %s with %s', method, parameter)
await isPortOpenRetry(CONFIG.NODE_SERVER_URL)
return client.exec<T>(method, parameter)
}
// template rpcCall, check first if port is open before executing json rpc 2.0 request, throw error on failure, return result on success
export async function rpcCallResolved<T>(method: string, parameter: any): Promise<T> {
const response = await rpcCall<WithTimeUsed<T>>(method, parameter)
return resolveResponse(response, (result: WithTimeUsed<T>) => {
if (result.timeUsed) {
logger.info(`call %s, used ${result.timeUsed}`, method)
}
return result as T
})
}

View File

@ -1,10 +1,10 @@
import { gql, GraphQLClient } from 'graphql-request'
import { SignJWT } from 'jose'
import { CONFIG } from '../config'
import { communitySchema, type Community } from '../schemas/rpcParameter.schema'
import { CONFIG } from '../../config'
import { communitySchema, type Community } from './community.schema'
import { getLogger, Logger } from 'log4js'
import { LOG4JS_BASE_CATEGORY } from '../config/const'
import { LOG4JS_BASE_CATEGORY } from '../../config/const'
import * as v from 'valibot'
const homeCommunity = gql`

View File

@ -1,10 +1,10 @@
import { communitySchema } from './rpcParameter.schema'
import { uuidv4Schema } from './typeGuard.schema'
import { communitySchema } from './community.schema'
import { uuidv4Schema } from '../../schemas/typeGuard.schema'
import * as v from 'valibot'
// only for IDE, bun don't need this to work
import { describe, expect, it } from 'bun:test'
describe('rpcParameter.schema', () => {
describe('community.schema', () => {
it('community', () => {
expect(v.parse(communitySchema, {
uuid: '4f28e081-5c39-4dde-b6a4-3bde71de8d65',

View File

@ -1,6 +1,6 @@
import * as v from 'valibot'
import { uuidv4Schema } from './typeGuard.schema'
import { dateSchema } from './typeConverter.schema'
import { uuidv4Schema } from '../../schemas/typeGuard.schema'
import { dateSchema } from '../../schemas/typeConverter.schema'
/**
* Schema Definitions for rpc call parameter, when dlt-connector is called from backend

View File

@ -0,0 +1,33 @@
import { MemoryBlock } from 'gradido-blockchain-js'
import { Uuidv4, Hex32, hex32Schema, MemoryBlock32, memoryBlock32Schema } from '../schemas/typeGuard.schema'
import * as v from 'valibot'
/**
* Uuidv4Hash is a class that represents a uuidv4 BLAKE2b hash
* to get the hash, the - in the uuidv4 will be removed and the result interpreted as hex string
* then the hash will be calculated with BLAKE2b algorithm
* crypto_generichash from libsodium will be called when calling MemoryBlock.calculateHash()
*
* This will be used as NameHash for user identification (user uuid as input)
* This will be used as IotaTopic for transactions (community uuid as input)
*/
export class Uuidv4Hash {
uuidv4Hash: MemoryBlock32
// used for caching hex string representation of uuidv4Hash
uuidv4HashString: Hex32 | undefined
constructor(uuidv4: Uuidv4) {
this.uuidv4Hash = v.parse(memoryBlock32Schema, MemoryBlock.fromHex(uuidv4.replace(/-/g, '')).calculateHash())
}
getAsMemoryBlock(): MemoryBlock32 {
return this.uuidv4Hash
}
getAsHexString(): Hex32 {
if (!this.uuidv4HashString) {
this.uuidv4HashString = v.parse(hex32Schema, this.uuidv4Hash)
}
return this.uuidv4HashString
}
}

View File

@ -10,5 +10,6 @@ export enum AccountType {
COMMUNITY_AUF = 'COMMUNITY_AUF', // community compensation and environment founds account
COMMUNITY_PROJECT = 'COMMUNITY_PROJECT', // no creations allowed
SUBACCOUNT = 'SUBACCOUNT', // no creations allowed
CRYPTO_ACCOUNT = 'CRYPTO_ACCOUNT', // user control his keys, no creations
CRYPTO_ACCOUNT = 'CRYPTO_ACCOUNT', // user control his keys, no creations,
DEFERRED_TRANSFER = 'DEFERRED_TRANSFER', // no creations allowed
}

View File

@ -6,6 +6,7 @@ import {
AddressType_CRYPTO_ACCOUNT,
AddressType_NONE,
AddressType_SUBACCOUNT,
AddressType_DEFERRED_TRANSFER,
} from 'gradido-blockchain-js'
export enum AddressType {
@ -16,4 +17,5 @@ export enum AddressType {
CRYPTO_ACCOUNT = AddressType_CRYPTO_ACCOUNT,
NONE = AddressType_NONE,
SUBACCOUNT = AddressType_SUBACCOUNT,
DEFERRED_TRANSFER = AddressType_DEFERRED_TRANSFER,
}

View File

@ -4,11 +4,13 @@ import { loadCryptoKeys, MemoryBlock } from 'gradido-blockchain-js'
import { getLogger, configure } from 'log4js'
import { readFileSync } from 'node:fs'
import { isPortOpenRetry } from './utils/network'
import { BackendClient } from './client/BackendClient'
import { BackendClient } from './client/backend/BackendClient'
import { KeyPairCacheManager } from './KeyPairCacheManager'
import { communityUuidToTopicSchema } from './schemas/rpcParameter.schema'
import { parse } from 'valibot'
import { getTransaction } from './client/GradidoNode/jsonrpc.api'
import { getTransaction } from './client/GradidoNode/api'
import { Uuidv4Hash } from './data/Uuidv4Hash'
import { SendToIotaContext } from './interactions/sendToIota/SendToIota.context'
import { keyGenerationSeedSchema } from './schemas/base.schema'
import * as v from 'valibot'
async function main() {
// configure log4js
@ -16,21 +18,12 @@ async function main() {
const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8'))
configure(options)
const logger = getLogger('dlt')
// TODO: replace with schema validation
if (CONFIG.IOTA_HOME_COMMUNITY_SEED) {
try {
const seed = MemoryBlock.fromHex(CONFIG.IOTA_HOME_COMMUNITY_SEED)
if (seed.size() < 32) {
throw new Error('seed need to be greater than 32 Bytes')
}
} catch (error) {
logger.error(
'IOTA_HOME_COMMUNITY_SEED must be a valid hex string, at least 64 characters long',
)
process.exit(1)
}
// check if valid seed for root key pair generation is present
if (!v.safeParse(keyGenerationSeedSchema, CONFIG.IOTA_HOME_COMMUNITY_SEED).success) {
logger.error('IOTA_HOME_COMMUNITY_SEED must be a valid hex string, at least 64 characters long')
process.exit(1)
}
// load crypto keys for gradido blockchain lib
loadCryptoKeys(
MemoryBlock.fromHex(CONFIG.GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET),
@ -46,19 +39,19 @@ async function main() {
await isPortOpenRetry(CONFIG.BACKEND_SERVER_URL)
const homeCommunity = await backend.getHomeCommunityDraft()
KeyPairCacheManager.getInstance().setHomeCommunityUUID(homeCommunity.uuid)
logger.info('home community topic: %s', parse(communityUuidToTopicSchema, homeCommunity.uuid))
const topic = new Uuidv4Hash(homeCommunity.uuid).getAsHexString()
logger.info('home community topic: %s', topic)
logger.info('gradido node server: %s', CONFIG.NODE_SERVER_URL)
// ask gradido node if community blockchain was created
try {
const topic = parse(communityUuidToTopicSchema, homeCommunity.uuid)
if (!await getTransaction(1, topic)) {
if (!await getTransaction({ transactionNr: 1, topic })) {
// if not exist, create community root transaction
await SendToIotaContext(homeCommunity)
}
} catch (e) {
logger.error('error requesting gradido node: ', e)
}
// listen for rpc request from backend (replace graphql with json rpc)
const app = new Elysia()
.get('/', () => "Hello Elysia")
.listen(CONFIG.DLT_CONNECTOR_PORT, () => {

View File

@ -1,5 +1,5 @@
import { KeyPairEd25519 } from 'gradido-blockchain-js'
import { communityUuidToTopicSchema } from '../../schemas/rpcParameter.schema'
import { communityUuidToTopicSchema } from '../../client/backend/community.schema'
import * as v from 'valibot'
export abstract class AbstractRemoteKeyPairRole {

View File

@ -1,6 +1,6 @@
import { KeyPairEd25519 } from 'gradido-blockchain-js'
import { getTransaction } from '../../client/GradidoNode/jsonrpc.api'
import { getTransaction } from '../../client/GradidoNode/api'
import { AbstractRemoteKeyPairRole } from './AbstractRemoteKeyPair.role'
import { GradidoNodeInvalidTransactionError, GradidoNodeMissingTransactionError } from '../../errors'

View File

@ -1,6 +1,6 @@
import { KeyPairEd25519 } from 'gradido-blockchain-js'
import { findUserByNameHash } from '../../client/GradidoNode/jsonrpc.api'
import { findUserByNameHash } from '../../client/GradidoNode/api'
import { IdentifierAccount } from '../../schemas/account.schema'
import { GradidoNodeMissingUserError, ParameterError } from '../../errors'
import { AbstractRemoteKeyPairRole } from './AbstractRemoteKeyPair.role'

View File

@ -10,7 +10,7 @@ import {
import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.context'
import { AbstractTransactionRole } from './AbstractTransaction.role'
import { Community, CommunityInput, communitySchema } from '../../schemas/rpcParameter.schema'
import { Community, CommunityInput, communitySchema } from '../../client/backend/community.schema'
import * as v from 'valibot'
export class CommunityRootTransactionRole extends AbstractTransactionRole {

View File

@ -1,6 +1,5 @@
import * as v from 'valibot'
import { uuidv4Schema } from './typeConverter.schema'
import { uuidv4Schema } from './typeGuard.schema'
// use code from transaction links
export const identifierSeedSchema = v.object({

View File

@ -0,0 +1,11 @@
import * as v from 'valibot'
import { MemoryBlock } from 'gradido-blockchain-js'
export const keyGenerationSeedSchema = v.pipe(
v.string('expect string type'),
v.hexadecimal('expect hexadecimal string'),
v.length(64, 'expect seed length minimum 64 characters (32 Bytes)'),
v.transform<string, MemoryBlock>(
(input: string) => MemoryBlock.fromHex(input),
),
)

View File

@ -4,29 +4,9 @@ import { identifierAccountSchema } from './account.schema'
import { InputTransactionType } from '../enum/InputTransactionType'
import { accountTypeToAddressTypeSchema } from './typeConverter.schema'
// allow TransactionIdentifier to only contain either transactionNr or iotaMessageId
export const transactionIdentifierSchema = v.pipe(
v.object({
transactionNr: v.nullish(
v.pipe(v.number('expect number type'), v.minValue(0, 'expect number >= 0')),
0
),
iotaMessageId: v.nullish(iotaMessageIdSchema, undefined),
communityId: uuid4ToTopicSchema,
}),
v.custom((value: any) => {
const setFieldsCount = Number(value.transactionNr !== 0) + Number(value.iotaMessageId !== undefined)
if (setFieldsCount !== 1) {
return false
}
return true
}, 'expect transactionNr or iotaMessageId not both')
)
export type TransactionIdentifierInput = v.InferInput<typeof transactionIdentifierSchema>
export type TransactionIdentifier = v.InferOutput<typeof transactionIdentifierSchema>
export const transactionSchema = v.object({
user: identifierAccountSchema,
user: identifierAccountScmhema,
linkedUser: v.nullish(identifierAccountSchema, undefined),
amount: v.nullish(amountToGradidoUnitSchema, undefined),
memo: v.nullish(memoSchema, undefined),

View File

@ -1,10 +1,10 @@
import { accountTypeSchema, addressTypeSchema, confirmedTransactionFromBase64Schema } from './typeConverter.schema'
import { accountTypeSchema, addressTypeSchema, confirmedTransactionSchema } from './typeConverter.schema'
import * as v from 'valibot'
// only for IDE, bun don't need this to work
import { describe, expect, it } from 'bun:test'
import { dateSchema } from './typeConverter.schema'
import { AddressType_COMMUNITY_AUF } from 'gradido-blockchain-js'
import { AddressType_COMMUNITY_AUF, AddressType_COMMUNITY_PROJECT } from 'gradido-blockchain-js'
import { AccountType } from '../enum/AccountType'
describe('basic.schema', () => {
@ -24,6 +24,10 @@ describe('basic.schema', () => {
})
describe('AddressType and AccountType', () => {
it('AddressType from string', () => {
const addressType = v.parse(addressTypeSchema, 'COMMUNITY_AUF')
expect(addressType).toBe(AddressType_COMMUNITY_AUF)
})
it('AddressType from AddressType', () => {
const addressType = v.parse(addressTypeSchema, AddressType_COMMUNITY_AUF)
expect(addressType).toBe(AddressType_COMMUNITY_AUF)
@ -42,8 +46,8 @@ describe('basic.schema', () => {
})
})
it('confirmedTransactionFromBase64Schema', () => {
const confirmedTransaction = v.parse(confirmedTransactionFromBase64Schema, 'CAcSAgoAGgYIwvK5/wUiAzMuNCogAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
it('confirmedTransactionSchema', () => {
const confirmedTransaction = v.parse(confirmedTransactionSchema, 'CAcSAgoAGgYIwvK5/wUiAzMuNCogAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
expect(confirmedTransaction.getId()).toBe(7)
expect(confirmedTransaction.getConfirmedAt().getSeconds()).toBe(1609464130)
expect(confirmedTransaction.getVersionNumber()).toBe('3.4')

View File

@ -1,20 +1,10 @@
import {
AddressType as AddressType,
AddressType_COMMUNITY_AUF,
AddressType_COMMUNITY_GMW,
AddressType_COMMUNITY_HUMAN,
AddressType_COMMUNITY_PROJECT,
AddressType_CRYPTO_ACCOUNT,
AddressType_NONE,
AddressType_SUBACCOUNT,
ConfirmedTransaction,
DeserializeType_CONFIRMED_TRANSACTION,
InteractionDeserialize,
MemoryBlock,
} from 'gradido-blockchain-js'
import { AccountType } from '../enum/AccountType'
// import { AddressType as AddressTypeWrapper } from '../enum/AddressType'
import * as v from 'valibot'
import { confirmedTransactionFromBase64, isAddressType, toAddressType, toAccountType } from '../utils/typeConverter'
/**
* dateSchema for creating a date from string or Date object
@ -42,45 +32,14 @@ export const dateSchema = v.pipe(
* AddressType is defined in gradido-blockchain C++ Code
* AccountType is the enum defined in TypeScript but with the same options
* addressTypeSchema and accountTypeSchema are for easy handling and conversion between both
*/
const accountToAddressMap: Record<AccountType, AddressType> = {
[AccountType.COMMUNITY_AUF]: AddressType_COMMUNITY_AUF,
[AccountType.COMMUNITY_GMW]: AddressType_COMMUNITY_GMW,
[AccountType.COMMUNITY_HUMAN]: AddressType_COMMUNITY_HUMAN,
[AccountType.COMMUNITY_PROJECT]: AddressType_COMMUNITY_PROJECT,
[AccountType.CRYPTO_ACCOUNT]: AddressType_CRYPTO_ACCOUNT,
[AccountType.SUBACCOUNT]: AddressType_SUBACCOUNT,
[AccountType.NONE]: AddressType_NONE,
}
const addressToAccountMap: Record<AddressType, AccountType> = Object.entries(accountToAddressMap).reduce((acc, [accKey, addrVal]) => {
acc[addrVal] = String(accKey) as AccountType
return acc;
}, {} as Record<AddressType, AccountType>)
function isAddressType(val: unknown): val is AddressType {
return typeof val === 'number' && Object.keys(addressToAccountMap).includes(val.toString())
}
function isAccountType(val: unknown): val is AccountType {
return Object.values(AccountType).includes(val as AccountType);
}
/**
* Schema for address type, can also convert from account type (if used with v.parse)
*/
export const addressTypeSchema = v.pipe(
v.union([
v.enum(AccountType, 'expect account type'),
v.custom<AddressType>((val): val is AddressType => isAddressType(val), 'expect AddressType'),
v.custom<AddressType>(isAddressType, 'expect AddressType'),
]),
v.transform<AccountType | AddressType, AddressType>((value) => {
if (isAddressType(value)) {
return value;
}
return accountToAddressMap[value as AccountType] ?? AddressType_NONE
}),
v.transform<AccountType | AddressType, AddressType>((value) => toAddressType(value)),
)
/**
@ -91,34 +50,24 @@ export const accountTypeSchema = v.pipe(
v.custom<AddressType>(isAddressType, 'expect AddressType'),
v.enum(AccountType, 'expect AccountType'),
]),
v.transform<AddressType | AccountType, AccountType>((value) => {
if (isAccountType(value)) {
return value;
}
return addressToAccountMap[value as AddressType] ?? AccountType.NONE;
}),
v.transform<AddressType | AccountType, AccountType>((value) => toAccountType(value)),
)
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 Error("invalid data, couldn't deserialize")
}
return confirmedTransaction
}
export const confirmedTransactionFromBase64Schema = v.pipe(
v.pipe(
v.string('expect confirmed Transaction base64 as string type'),
v.base64('expect to be valid base64')
),
v.transform<string, ConfirmedTransaction>(
(base64: string) => confirmedTransactionFromBase64(base64),
export const confirmedTransactionSchema = v.pipe(
v.union([
v.instance(ConfirmedTransaction, 'expect ConfirmedTransaction'),
v.pipe(
v.string('expect confirmed Transaction base64 as string type'),
v.base64('expect to be valid base64')
),
]),
v.transform<string | ConfirmedTransaction, ConfirmedTransaction>(
(data: string | ConfirmedTransaction) => {
if (data instanceof ConfirmedTransaction) {
return data
}
return confirmedTransactionFromBase64(data)
},
),
)

View File

@ -1,67 +1,37 @@
import { describe, it, expect } from 'bun:test'
import { uuidv4Schema, topicIndexSchema, uuid4HashSchema, memoSchema } from './typeGuard.schema'
import { uuidv4Schema, memoSchema } from './typeGuard.schema'
import * as v from 'valibot'
import { v4 as uuidv4 } from 'uuid'
import { MemoryBlock } from 'gradido-blockchain-js'
describe('typeGuard.schema', () => {
describe('Uuidv4', () => {
const uuidv4String = uuidv4()
const uuidv4Hash = MemoryBlock.fromHex(uuidv4String.replace(/-/g, '')).calculateHash()
it('from string to uuidv4', () => {
const uuidv4Value = v.parse(uuidv4Schema, uuidv4String)
expect(uuidv4Value.toString()).toBe(uuidv4String)
})
it('from uuidv4 to hash', () => {
const uuidv4Value = v.parse(uuidv4Schema, uuidv4String)
const uuidv4HashParsed = v.parse(uuid4HashSchema, uuidv4Value)
expect(uuidv4HashParsed.copyAsString()).toBe(uuidv4Hash.copyAsString())
})
it('from uuidv4 string to hash', () => {
const uuidv4HashParsed = v.parse(uuid4HashSchema, uuidv4String)
expect(uuidv4HashParsed.copyAsString()).toBe(uuidv4Hash.copyAsString())
})
it('from uuidv4 hash to topicIndex (hash in hex format', () => {
const uuidv4HashParsed = v.parse(uuid4HashSchema, uuidv4String)
const topicIndex = v.parse(topicIndexSchema, uuidv4HashParsed)
expect(topicIndex.toString()).toBe(uuidv4Hash.convertToHex())
})
it('from uuidv4 to topicIndex (hash in hex format)', () => {
const uuidv4Value = v.parse(uuidv4Schema, uuidv4String)
const topicIndex = v.parse(topicIndexSchema, uuidv4Value)
expect(topicIndex.toString()).toBe(uuidv4Hash.convertToHex())
})
it('from uuidv4 string to topicIndex (hash in hex format)', () => {
const topicIndex = v.parse(topicIndexSchema, uuidv4String)
expect(topicIndex.toString()).toBe(uuidv4Hash.convertToHex())
})
})
describe('Basic Type Schemas for transactions', () => {
describe('Memo', () => {
it('min length', () => {
const memoValue = 'memo1'
const memoValueParsed = v.parse(memoSchema, memoValue)
expect(memoValueParsed.toString()).toBe(memoValue)
})
it('max length', () => {
const memoValue = 's'.repeat(255)
const memoValueParsed = v.parse(memoSchema, memoValue)
expect(memoValueParsed.toString()).toBe(memoValue)
})
it('to short', () => {
const memoValue = 'memo'
expect(() => v.parse(memoSchema, memoValue)).toThrow(new Error('expect string length >= 5'))
})
it('to long', () => {
const memoValue = 's'.repeat(256)
expect(() => v.parse(memoSchema, memoValue)).toThrow(new Error('expect string length <= 255'))
})
describe('Memo', () => {
it('min length', () => {
const memoValue = 'memo1'
const memoValueParsed = v.parse(memoSchema, memoValue)
expect(memoValueParsed.toString()).toBe(memoValue)
})
it('max length', () => {
const memoValue = 's'.repeat(255)
const memoValueParsed = v.parse(memoSchema, memoValue)
expect(memoValueParsed.toString()).toBe(memoValue)
})
it('to short', () => {
const memoValue = 'memo'
expect(() => v.parse(memoSchema, memoValue)).toThrow(new Error('expect string length >= 5'))
})
it('to long', () => {
const memoValue = 's'.repeat(256)
expect(() => v.parse(memoSchema, memoValue)).toThrow(new Error('expect string length <= 255'))
})
})
})
})

View File

@ -13,6 +13,7 @@
* `const amount: Amount = v.parse(amountSchema, 1.21)`
* must be called and ensure the value is valid
* If it isn't valid, v.parse will throw an error
* Alternatively v.safeParse can be used, this don't throw but it return null on error
*/
import { validate, version } from 'uuid'
@ -27,48 +28,89 @@ import { MemoryBlock, DurationSeconds, GradidoUnit } from 'gradido-blockchain-js
declare const validUuidv4: unique symbol
export type Uuidv4 = string & { [validUuidv4]: true };
export const uuidv4Schema = v.custom<Uuidv4>((value) =>
(typeof value === 'string' && validate(value) && version(value) === 4),
'uuid v4 expected'
export const uuidv4Schema = v.pipe(
v.string('expect string type'),
v.custom<string>((value) =>
(typeof value === 'string' && validate(value) && version(value) === 4),
'uuid v4 expected'
),
v.transform<string, Uuidv4>(
(input: string) => input as Uuidv4,
),
)
export type Uuidv4Input = v.InferInput<typeof uuidv4Schema>
/**
* type guard for memory block size 32
* create with `v.parse(memoryBlock32Schema, MemoryBlock.fromHex('39568d7e148a0afee7f27a67dbf7d4e87d1fdec958e2680df98a469690ffc1a2'))`
* memoryBlock32 is a non-empty MemoryBlock with size 32
*/
declare const validMemoryBlock32: unique symbol
export type MemoryBlock32 = MemoryBlock & { [validMemoryBlock32]: true };
export const memoryBlock32Schema = v.pipe(
v.instance(MemoryBlock, 'expect MemoryBlock type'),
v.custom<MemoryBlock>(
(val): boolean => val instanceof MemoryBlock && val.size() === 32 && !val.isEmpty(),
'expect MemoryBlock size = 32 and not empty'
),
v.transform<MemoryBlock, MemoryBlock32>(
(input: MemoryBlock) => input as MemoryBlock32,
),
)
/**
* type guard for uuid v4 hash
* const uuidv4Value: Uuidv4 = v.parse(uuidv4Schema, 'uuid')
* create with `v.parse(uuidv4HashSchema, uuidv4Value)`
* uuidv4Hash is uuidv4 value hashed with BLAKE2b as Binary Type MemoryBlock from gradido-blockchain similar to Node.js Buffer Type,
* used for iota topic
* type guard for hex string of length 64 (binary size = 32)
* create with `v.parse(hex32Schema, '39568d7e148a0afee7f27a67dbf7d4e87d1fdec958e2680df98a469690ffc1a2')`
* or `v.parse(hex32Schema, MemoryBlock.fromHex('39568d7e148a0afee7f27a67dbf7d4e87d1fdec958e2680df98a469690ffc1a2'))`
* hex32 is a hex string of length 64 (binary size = 32)
*/
declare const validUuidv4Hash: unique symbol
export type Uuidv4Hash = MemoryBlock & { [validUuidv4Hash]: true };
declare const validHex32: unique symbol
export type Hex32 = string & { [validHex32]: true };
export const uuid4HashSchema = v.pipe(
uuidv4Schema,
v.transform<Uuidv4, Uuidv4Hash>(
(input: Uuidv4) => MemoryBlock.fromHex(input.replace(/-/g, '')).calculateHash() as Uuidv4Hash,
)
export const hex32Schema = v.pipe(
v.union([
v.pipe(
v.string('expect string type'),
v.hexadecimal('expect hexadecimal string'),
v.length(64, 'expect string length = 64'),
),
memoryBlock32Schema,
]),
v.transform<string | MemoryBlock32 | Hex32, Hex32>(
(input: string | MemoryBlock32 | Hex32) => {
if (typeof input === 'string') {
return input as Hex32
}
return input.convertToHex() as Hex32
},
),
)
/**
* type guard for topic index
* const uuidv4Value: Uuidv4 = v.parse(uuidv4Schema, 'uuid')
* const uuidv4Hash: Uuidv4Hash = v.parse(uuid4HashSchema, uuidv4Value)
* create with `v.parse(topicIndexSchema, uuidv4Hash)`
* topicIndex is uuidv4Hash value converted to hex string used for iota topic
* The beauty of valibot allow also parse a uuidv4 string directly to topicIndex
* const topic: TopicIndex = v.parse(topicIndexSchema, 'uuid')
*/
declare const validTopicIndex: unique symbol
export type TopicIndex = string & { [validTopicIndex]: true };
export type Hex32Input = v.InferInput<typeof hex32Schema>
export const topicIndexSchema = v.pipe(
v.union([uuidv4Schema, v.custom((val): val is Uuidv4Hash => val instanceof MemoryBlock)]),
v.transform<any, TopicIndex>((input) => {
const hash = typeof input === 'string'
? MemoryBlock.fromHex(input.replace(/-/g, '')).calculateHash()
: input;
return hash.convertToHex() as TopicIndex;
})
/**
* type guard for iota message id
* create with `v.parse(iotaMessageIdSchema, '822387692a7cfd3f07f25742e91e248af281d771ee03a432c2e178e5533f786c')`
* iota message id is a hex string of length 64
*/
declare const validIotaMessageId: unique symbol
export type IotaMessageId = Hex32 & { [validIotaMessageId]: true };
export const iotaMessageIdSchema = v.pipe(
v.union([
v.pipe(
v.string('expect string type'),
v.hexadecimal('expect hexadecimal string'),
v.length(64, 'expect string length = 64'),
),
memoryBlock32Schema,
]),
v.transform<string | MemoryBlock32, IotaMessageId>(
(input: string | MemoryBlock32) => v.parse(hex32Schema, input) as IotaMessageId,
),
)
/**
@ -133,7 +175,8 @@ export const amountSchema = v.pipe(
/**
* type guard for gradido amount
* create with `v.parse(gradidoAmountSchema, '123')`
* gradido amount is a string representing a positive decimal number, compatible with decimal.js
* gradido amount is a GradidoUnit representing a positive gradido amount stored intern as integer with 4 decimal places
* GradidoUnit is native implemented in gradido-blockchain-js in c++ and has functions for decay calculation
*/
declare const validGradidoAmount: unique symbol
export type GradidoAmount = GradidoUnit & { [validGradidoAmount]: true };

View File

@ -0,0 +1,77 @@
import { initTRPC, TRPCError } from '@trpc/server'
import * as v from 'valibot'
import { identifierAccountSchema } from '../schemas/account.schema'
import { KeyPairIdentifierLogic } from '../data/KeyPairIdentifier.logic'
import { getLogger } from 'log4js'
import { LOG4JS_BASE_CATEGORY } from '../config/const'
import { KeyPairCalculation } from '../interactions/keyPairCalculation/KeyPairCalculation.context'
import { getAddressType } from '../client/GradidoNode/api'
import { Uuidv4Hash } from '../data/Uuidv4Hash'
import { hex32Schema } from '../schemas/typeGuard.schema'
import { AddressType_NONE } from 'gradido-blockchain-js'
export const t = initTRPC.create()
const publicProcedure = t.procedure
const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.server`)
export const appRouter = t.router({
isAccountExist: publicProcedure
.input(identifierAccountSchema)
.output(v.boolean())
.query(async ({ input: userIdentifier }) => {
const accountKeyPair = await KeyPairCalculation(new KeyPairIdentifierLogic(userIdentifier))
const publicKey = accountKeyPair.getPublicKey()
if (!publicKey) {
throw new TRPCError({
code: 'NOT_FOUND',
message: "couldn't calculate account key pair",
})
}
// ask gradido node server for account type, if type !== NONE account exist
const addressType = await getAddressType(
v.parse(hex32Schema, publicKey.get()),
new Uuidv4Hash(userIdentifier.communityUuid),
)
logger.info('isAccountExist')
if(logger.isDebugEnabled()) {
logger.debug('params', userIdentifier)
}
return addressType !== AddressType_NONE
}),
sendTransaction: publicProcedure
.input(transactionDraftSchema)
.output(v.instanceof(TransactionResult))
.mutation(async ({ input: transactionDraft }) => {
try {
return await SendToIotaContext(transactionDraft)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
if (error instanceof TransactionError) {
return new TransactionResult(error)
} else {
throw error
}
}
})
})
/*
async sendTransaction(
@Arg('data')
transactionDraft: TransactionDraft,
): Promise<TransactionResult> {
try {
return await SendToIotaContext(transactionDraft)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
if (error instanceof TransactionError) {
return new TransactionResult(error)
} else {
throw error
}
}
}
*/

View File

@ -0,0 +1,72 @@
import {
ConfirmedTransaction,
MemoryBlock,
InteractionDeserialize,
DeserializeType_CONFIRMED_TRANSACTION,
AddressType_COMMUNITY_AUF,
AddressType_COMMUNITY_GMW,
AddressType_COMMUNITY_HUMAN,
AddressType_COMMUNITY_PROJECT,
AddressType_CRYPTO_ACCOUNT,
AddressType_SUBACCOUNT,
AddressType_DEFERRED_TRANSFER,
AddressType_NONE,
AddressType,
} from 'gradido-blockchain-js'
import { AccountType } from '../enum/AccountType'
export const confirmedTransactionFromBase64 = (base64: string): ConfirmedTransaction => {
const confirmedTransactionBinaryPtr = MemoryBlock.createPtr(MemoryBlock.fromBase64(base64))
const deserializer = new InteractionDeserialize(
confirmedTransactionBinaryPtr,
DeserializeType_CONFIRMED_TRANSACTION,
)
deserializer.run()
const confirmedTransaction = deserializer.getConfirmedTransaction()
if (!confirmedTransaction) {
throw new Error("invalid data, couldn't deserialize")
}
return confirmedTransaction
}
/**
* AddressType is defined in gradido-blockchain C++ Code
* AccountType is the enum defined in TypeScript but with the same options
*/
const accountToAddressMap: Record<AccountType, AddressType> = {
[AccountType.COMMUNITY_AUF]: AddressType_COMMUNITY_AUF,
[AccountType.COMMUNITY_GMW]: AddressType_COMMUNITY_GMW,
[AccountType.COMMUNITY_HUMAN]: AddressType_COMMUNITY_HUMAN,
[AccountType.COMMUNITY_PROJECT]: AddressType_COMMUNITY_PROJECT,
[AccountType.CRYPTO_ACCOUNT]: AddressType_CRYPTO_ACCOUNT,
[AccountType.SUBACCOUNT]: AddressType_SUBACCOUNT,
[AccountType.DEFERRED_TRANSFER]: AddressType_DEFERRED_TRANSFER,
[AccountType.NONE]: AddressType_NONE,
}
const addressToAccountMap: Record<AddressType, AccountType> = Object.entries(accountToAddressMap).reduce((acc, [accKey, addrVal]) => {
acc[addrVal] = String(accKey) as AccountType
return acc;
}, {} as Record<AddressType, AccountType>)
export function isAddressType(val: unknown): val is AddressType {
return typeof val === 'number' && Object.keys(addressToAccountMap).includes(val.toString())
}
export function isAccountType(val: unknown): val is AccountType {
return Object.values(AccountType).includes(val as AccountType);
}
export function toAddressType(input: AccountType | AddressType): AddressType {
if (isAddressType(input)) {
return input
}
return accountToAddressMap[input as AccountType] ?? AddressType_NONE
}
export function toAccountType(input: AccountType | AddressType): AccountType {
if (isAccountType(input)) {
return input
}
return addressToAccountMap[input as AddressType] ?? AccountType.NONE
}