Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"test:cov": "npx jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "npx jest --config ./test/jest-e2e.json",
"test:ci": "npx jest --ci --runInBand",
"test:ci": "npx jest --ci --runInBand --forceExit",
"clear-cache": "npx jest --clearCache",
"clean:modules": "rimraf node_modules",
"deleteAllTransactions": "ts-node ./src/scripts/deleteAllDBTransactions.ts",
Expand Down
5 changes: 3 additions & 2 deletions apps/backend/src/jobs/autoSubmit.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ import TransactionService from '../transaction/transaction.service';
import { TransactionStatus } from '../transaction/status.enum';
import {
Transaction,
Client,
PublicKey,
TransactionResponse,
TransactionReceipt,
Status,
Client,
} from '@hiero-ledger/sdk';
import { buildHederaClient } from '../utils/clientFactory';
import { GetTransactionsResponseDto } from '../transaction/dto/get-transactions-response.dto';
import { hexToUint8Array } from '../utils/utils';
import { LoggerService } from '../logger/logger.service.js';
Expand Down Expand Up @@ -142,7 +143,7 @@ export default class AutoSubmitService {

async submit(transaction: GetTransactionsResponseDto): Promise<boolean> {
try {
const client: Client = Client.forName(transaction.network);
const client = buildHederaClient(transaction.network, transaction.consensus_nodes);

let deserializedTransaction = Transaction.fromBytes(
hexToUint8Array(transaction.transaction_message),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,28 @@ import {
IsIn,
IsInt,
IsNotEmpty,
IsOptional,
IsString,
Matches,
Min,
ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer';
import { hederaIdRegex, hexRegex } from '../../common/regexp';
import { RemoveHexPrefix } from '../../common/decorators/transform-hexPrefix.decorator';
import { Network } from '../network.enum';
import { Transform } from 'class-transformer';

export class ConsensusNodeDto {
@IsString()
@IsNotEmpty()
url: string;

@IsString()
@IsNotEmpty()
nodeId: string;
}

export class CreateTransactionRequestDto {
@ApiProperty({
description: 'The message to be signed by the keys',
Expand Down Expand Up @@ -112,6 +125,17 @@ export class CreateTransactionRequestDto {
)
network: Network;

@ApiProperty({
description:
'Consensus nodes for custom networks (required when network is "custom")',
required: false,
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => ConsensusNodeDto)
consensus_nodes?: ConsensusNodeDto[];

@ApiProperty({
description: 'The start date of the transaction in ISO 8601 format',
example: '2023-08-01T12:00:00Z',
Expand All @@ -129,6 +153,7 @@ export class CreateTransactionRequestDto {
threshold: number,
network: Network,
start_date: string,
consensus_nodes?: ConsensusNodeDto[],
) {
this.transaction_message = transaction_message;
this.description = description;
Expand All @@ -137,5 +162,6 @@ export class CreateTransactionRequestDto {
this.threshold = threshold;
this.network = network;
this.start_date = start_date;
this.consensus_nodes = consensus_nodes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export class GetTransactionsResponseDto {
network: string;
hedera_account_id: string;
start_date: string;
consensus_nodes: { url: string; nodeId: string }[] | null;

constructor(
id: string,
Expand All @@ -43,6 +44,7 @@ export class GetTransactionsResponseDto {
network: string,
hedera_account_id: string,
start_date: string,
consensus_nodes: { url: string; nodeId: string }[] | null,
) {
this.id = id;
this.transaction_message = transaction_message;
Expand All @@ -55,5 +57,6 @@ export class GetTransactionsResponseDto {
this.network = network;
this.hedera_account_id = hedera_account_id;
this.start_date = start_date;
this.consensus_nodes = consensus_nodes;
}
}
1 change: 1 addition & 0 deletions apps/backend/src/transaction/network.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export enum Network {
MAINNET = 'mainnet',
TESTNET = 'testnet',
PREVIEWNET = 'previewnet',
CUSTOM = 'custom',
}
12 changes: 5 additions & 7 deletions apps/backend/src/transaction/transaction.entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Network } from './network.enum';
import { TransactionStatus } from './status.enum';

@Entity()
Expand Down Expand Up @@ -46,12 +45,11 @@ export default class Transaction {
@Column()
threshold: number;

@Column({
type: 'enum',
enum: Network,
nullable: false,
})
network: Network;
@Column({ type: 'varchar', nullable: false })
network: string;

@Column({ type: 'jsonb', nullable: true })
consensus_nodes: { url: string; nodeId: string }[] | null;

@Column({
type: 'timestamp with time zone',
Expand Down
6 changes: 4 additions & 2 deletions apps/backend/src/transaction/transaction.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ import {
} from '../common/exceptions/domain-exceptions';
import { TransactionStatus } from './status.enum';
import { Network } from './network.enum';
import { Client, Transaction as TransactionSdk } from '@hiero-ledger/sdk';
import { Transaction as TransactionSdk } from '@hiero-ledger/sdk';
import { buildHederaClient } from '../utils/clientFactory';

@Injectable()
export default class TransactionService {
Expand Down Expand Up @@ -97,7 +98,7 @@ export default class TransactionService {

const deserializedTransaction = TransactionSdk.fromBytes(
hexToUint8Array(transaction.transaction_message),
).freezeWith(Client.forName(transaction.network));
).freezeWith(buildHederaClient(transaction.network, transaction.consensus_nodes));

if (
!verifySignature(
Expand Down Expand Up @@ -251,6 +252,7 @@ export default class TransactionService {
transaction.network,
transaction.hedera_account_id,
transaction.start_date.toUTCString(),
transaction.consensus_nodes ?? null,
);
}
}
44 changes: 44 additions & 0 deletions apps/backend/src/utils/clientFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
*
* Hedera Stablecoin SDK
*
* Copyright (C) 2023 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

import { Client } from '@hiero-ledger/sdk';

export function buildHederaClient(
network: string,
consensusNodes?: { url: string; nodeId: string }[] | null,
): Client {
switch (network) {
case 'mainnet':
return Client.forMainnet();
case 'testnet':
return Client.forTestnet();
case 'previewnet':
return Client.forPreviewnet();
default: {
if (!consensusNodes?.length)
throw new Error(
`Network '${network}' requires consensus_nodes to be provided`,
);
return Client.forNetwork(
Object.fromEntries(consensusNodes.map((n) => [n.url, n.nodeId])),
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ describe('Transaction Controller Test', () => {
DEFAULT.network,
DEFAULT.hedera_account_id,
DEFAULT.start_date.toDateString(),
null,
),
),
);
Expand All @@ -349,6 +350,7 @@ describe('Transaction Controller Test', () => {
DEFAULT.network,
DEFAULT.hedera_account_id,
DEFAULT.start_date.toDateString(),
null,
);
//* 🎬 Act ⬇
const result = await controller.getTransactionById(
Expand Down Expand Up @@ -381,6 +383,7 @@ function createMockGetAllByPublicKeyTxServiceResult(
pendingTransaction.network,
pendingTransaction.hedera_account_id,
pendingTransaction.start_date.toDateString(),
null,
);
return new Pagination<GetTransactionsResponseDto>(
[transactionResponse, transactionResponse],
Expand Down
1 change: 1 addition & 0 deletions apps/backend/test/transaction/transaction.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export default class TransactionMock extends Transaction {
signatures: TransactionMock.txPending0().signatures,
network: TransactionMock.txPending0().network,
start_date: TransactionMock.txPending0().start_date.toDateString(),
consensus_nodes: null,
};

static txPending1(command: Partial<TransactionMockCommand> = {}) {
Expand Down
9 changes: 5 additions & 4 deletions apps/backend/test/transaction/transaction.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { Repository } from 'typeorm';
import TransactionService from '../../src/transaction/transaction.service';
import Transaction from '../../src/transaction/transaction.entity';
import { SignTransactionRequestDto } from '../../src/transaction/dto/sign-transaction-request.dto';
import { CreateTransactionRequestDto } from '../../src/transaction/dto/create-transaction-request.dto';
import TransactionMock, { DEFAULT } from './transaction.mock';
import { LoggerService } from '../../src/logger/logger.service';
import { TransactionStatus } from '../../src/transaction/status.enum';
Expand Down Expand Up @@ -88,7 +89,7 @@ describe('Transaction Service Test', () => {
threshold: pendingTransaction.threshold,
network: pendingTransaction.network,
start_date: pendingTransaction.start_date.toDateString(),
};
} as CreateTransactionRequestDto;

const expected = TransactionMock.txPending0();
//* 🎬 Act ⬇
Expand Down Expand Up @@ -118,7 +119,7 @@ describe('Transaction Service Test', () => {
threshold: pendingTransaction.threshold,
network: pendingTransaction.network,
start_date: pendingTransaction.start_date.toDateString(),
};
} as CreateTransactionRequestDto;

//* 🎬 Act ⬇
const transaction = await service.create(createTransactionDto);
Expand All @@ -143,7 +144,7 @@ describe('Transaction Service Test', () => {
threshold: new_threshold,
network: pendingTransaction.network,
start_date: pendingTransaction.start_date.toDateString(),
};
} as CreateTransactionRequestDto;

//* 🎬 Act ⬇
const transaction = await service.create(createTransactionDto);
Expand All @@ -167,7 +168,7 @@ describe('Transaction Service Test', () => {
threshold: pendingTransaction.threshold,
network: pendingTransaction.network,
start_date: pendingTransaction.start_date.toDateString(),
};
} as CreateTransactionRequestDto;

const expected = TransactionMock.txPending0({
threshold: createTransactionDto.key_list.length,
Expand Down
4 changes: 4 additions & 0 deletions packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
"import": "./build/typechain-types/index.js",
"require": "./build/typechain-types/index.js"
},
"./typechain-types/factories/contracts": {
"import": "./build/typechain-types/factories/contracts/index.js",
"require": "./build/typechain-types/factories/contracts/index.js"
},
"./typechain-types/*": {
"import": "./build/typechain-types/*",
"require": "./build/typechain-types/*"
Expand Down
2 changes: 2 additions & 0 deletions packages/sdk/__mocks__/fireblocks-sdk.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// __mocks__/fireblocks-sdk.js
module.exports = {};
27 changes: 26 additions & 1 deletion packages/sdk/example/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,29 @@ FACTORY_ADDRESS='0.0.XXXX'
# Optional: provide an existing token ID to skip creation in testExternalEVM
TOKEN_ID=''
# Optional: provide an existing token ID to skip creation in testExternalHedera
TOKEN_ID_HEDERA=''
TOKEN_ID_HEDERA=''

# --- Multisig example (multisigFreeze.ts) ---
# Multisig account — the account whose keys are managed via the backend
MULTISIG_ACCOUNT_ID='0.0.XXXX'
# Backend URL for multisig transaction coordination
BACKEND_URL='http://127.0.0.1:3001/api/transactions/'
# Custom consensus node
CONSENSUS_NODE_URL='host:port'
CONSENSUS_NODE_ID='0.0.X'

# --- DFNS multisig example (createDFNSMultisigAccount.ts / multisigFreezeDFNS.ts) ---
# Multisig account created by createDFNSMultisigAccount.ts (KeyList includes DFNS key)
DFNS_MULTISIG_ACCOUNT_ID='0.0.XXXX'
# DFNS service account credentials
DFNS_SERVICE_ACCOUNT_AUTHORIZATION_TOKEN=''
DFNS_SERVICE_ACCOUNT_CREDENTIAL_ID=''
DFNS_SERVICE_ACCOUNT_PRIVATE_KEY_OR_PATH=''
# DFNS application settings
DFNS_APP_ORIGIN=''
DFNS_APP_ID=''
DFNS_BASE_URL=''
# DFNS wallet linked to the Hedera account
DFNS_WALLET_ID=''
DFNS_WALLET_PUBLIC_KEY=''
DFNS_HEDERA_ACCOUNT_ID='0.0.XXXX'
Loading
Loading