Skip to content
Merged
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
12 changes: 6 additions & 6 deletions .github/workflows/pr-review-bot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,18 @@ jobs:
continue-on-error: true
run: |
npm run lint 2>&1 | tee eslint-output.txt || true
WARNINGS=$(grep -c "warning" eslint-output.txt || echo "0")
ERRORS=$(grep -c "error" eslint-output.txt || echo "0")
echo "warnings=$WARNINGS" >> $GITHUB_OUTPUT
echo "errors=$ERRORS" >> $GITHUB_OUTPUT
WARNINGS=$(grep -c "warning" eslint-output.txt || true)
ERRORS=$(grep -c "error" eslint-output.txt || true)
echo "warnings=${WARNINGS:-0}" >> $GITHUB_OUTPUT
echo "errors=${ERRORS:-0}" >> $GITHUB_OUTPUT

- name: Run TypeScript check
id: typescript
continue-on-error: true
run: |
npx tsc --noEmit 2>&1 | tee tsc-output.txt || true
ERRORS=$(grep -c "error TS" tsc-output.txt || echo "0")
echo "errors=$ERRORS" >> $GITHUB_OUTPUT
ERRORS=$(grep -c "error TS" tsc-output.txt || true)
echo "errors=${ERRORS:-0}" >> $GITHUB_OUTPUT

- name: Security Audit
id: audit
Expand Down
52 changes: 52 additions & 0 deletions migration/1768341824012-AddCustodyAccountTables.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
* @typedef {import('typeorm').QueryRunner} QueryRunner
*/

/**
* @class
* @implements {MigrationInterface}
*/
module.exports = class AddCustodyAccountTables1768341824012 {
name = 'AddCustodyAccountTables1768341824012'

/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "custody_account_access" ("id" int NOT NULL IDENTITY(1,1), "updated" datetime2 NOT NULL CONSTRAINT "DF_7de1867f044392358470c9ade75" DEFAULT getdate(), "created" datetime2 NOT NULL CONSTRAINT "DF_8b3a56557a24d8c9157c4867435" DEFAULT getdate(), "accessLevel" nvarchar(255) NOT NULL, "accountId" int NOT NULL, "userDataId" int NOT NULL, CONSTRAINT "PK_1657b7fd1d5a0ee0b01f657e508" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_380e225bfd7707fff0e4f98035" ON "custody_account_access" ("accountId", "userDataId") `);
await queryRunner.query(`CREATE TABLE "custody_account" ("id" int NOT NULL IDENTITY(1,1), "updated" datetime2 NOT NULL CONSTRAINT "DF_91a0617046b1f9ca14218362617" DEFAULT getdate(), "created" datetime2 NOT NULL CONSTRAINT "DF_281789479a65769e9189e24f7d9" DEFAULT getdate(), "title" nvarchar(256) NOT NULL, "description" nvarchar(MAX), "requiredSignatures" int NOT NULL CONSTRAINT "DF_980d2b28fe8284b5060c70a36fd" DEFAULT 1, "status" nvarchar(255) NOT NULL CONSTRAINT "DF_aaeefb3ab36f3b7e02b5f4c67fc" DEFAULT 'Active', "ownerId" int NOT NULL, CONSTRAINT "PK_89fae3a990abaa76d843242fc6d" PRIMARY KEY ("id"))`);
await queryRunner.query(`ALTER TABLE "custody_order" ADD "accountId" int`);
await queryRunner.query(`ALTER TABLE "custody_order" ADD "initiatedById" int`);
await queryRunner.query(`ALTER TABLE "custody_balance" ADD "accountId" int`);
await queryRunner.query(`ALTER TABLE "user" ADD "custodyAccountId" int`);
await queryRunner.query(`ALTER TABLE "custody_account_access" ADD CONSTRAINT "FK_45213c9c7521d41be00fa5ead93" FOREIGN KEY ("accountId") REFERENCES "custody_account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "custody_account_access" ADD CONSTRAINT "FK_8a4612269b283bf40950ddb8485" FOREIGN KEY ("userDataId") REFERENCES "user_data"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "custody_account" ADD CONSTRAINT "FK_b89a7cbab6c121f5a092815fce3" FOREIGN KEY ("ownerId") REFERENCES "user_data"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "custody_order" ADD CONSTRAINT "FK_6a769cd0d90bc68cafdd533f03e" FOREIGN KEY ("accountId") REFERENCES "custody_account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "custody_order" ADD CONSTRAINT "FK_67425e623d89efe4ae1a48dbad6" FOREIGN KEY ("initiatedById") REFERENCES "user_data"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "custody_balance" ADD CONSTRAINT "FK_b141d5e0d74c87aef92eae2847a" FOREIGN KEY ("accountId") REFERENCES "custody_account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_bf8ce326ec41adc02940bccf91a" FOREIGN KEY ("custodyAccountId") REFERENCES "custody_account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}

/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_bf8ce326ec41adc02940bccf91a"`);
await queryRunner.query(`ALTER TABLE "custody_balance" DROP CONSTRAINT "FK_b141d5e0d74c87aef92eae2847a"`);
await queryRunner.query(`ALTER TABLE "custody_order" DROP CONSTRAINT "FK_67425e623d89efe4ae1a48dbad6"`);
await queryRunner.query(`ALTER TABLE "custody_order" DROP CONSTRAINT "FK_6a769cd0d90bc68cafdd533f03e"`);
await queryRunner.query(`ALTER TABLE "custody_account" DROP CONSTRAINT "FK_b89a7cbab6c121f5a092815fce3"`);
await queryRunner.query(`ALTER TABLE "custody_account_access" DROP CONSTRAINT "FK_8a4612269b283bf40950ddb8485"`);
await queryRunner.query(`ALTER TABLE "custody_account_access" DROP CONSTRAINT "FK_45213c9c7521d41be00fa5ead93"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "custodyAccountId"`);
await queryRunner.query(`ALTER TABLE "custody_balance" DROP COLUMN "accountId"`);
await queryRunner.query(`ALTER TABLE "custody_order" DROP COLUMN "initiatedById"`);
await queryRunner.query(`ALTER TABLE "custody_order" DROP COLUMN "accountId"`);
await queryRunner.query(`DROP TABLE "custody_account"`);
await queryRunner.query(`DROP INDEX "IDX_380e225bfd7707fff0e4f98035" ON "custody_account_access"`);
await queryRunner.query(`DROP TABLE "custody_account_access"`);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { createMock } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import { ArweaveService } from 'src/integration/blockchain/arweave/services/arweave.service';
import { CardanoService } from 'src/integration/blockchain/cardano/services/cardano.service';
import { MoneroService } from 'src/integration/blockchain/monero/services/monero.service';
import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum';
import { BlockchainRegistryService } from 'src/integration/blockchain/shared/services/blockchain-registry.service';
import { CryptoService } from 'src/integration/blockchain/shared/services/crypto.service';
import { SolanaService } from 'src/integration/blockchain/solana/services/solana.service';
import { SparkService } from 'src/integration/blockchain/spark/spark.service';
Expand All @@ -16,53 +17,24 @@ import { UserAddressType } from 'src/subdomains/generic/user/models/user/user.en
import { BitcoinService } from '../../node/bitcoin.service';

describe('CryptoService', () => {
let service: CryptoService;

let bitcoinService: BitcoinService;
let lightningService: LightningService;
let sparkService: SparkService;
let moneroService: MoneroService;
let zanoService: ZanoService;
let solanaService: SolanaService;
let tronService: TronService;
let cardanoService: CardanoService;
let arweaveService: ArweaveService;
let railgunService: RailgunService;

beforeEach(async () => {
bitcoinService = createMock<BitcoinService>();
lightningService = createMock<LightningService>();
sparkService = createMock<SparkService>();
moneroService = createMock<MoneroService>();
zanoService = createMock<ZanoService>();
solanaService = createMock<SolanaService>();
tronService = createMock<TronService>();
cardanoService = createMock<CardanoService>();
arweaveService = createMock<ArweaveService>();
railgunService = createMock<RailgunService>();

const module: TestingModule = await Test.createTestingModule({
await Test.createTestingModule({
providers: [
CryptoService,
{ provide: BitcoinService, useValue: bitcoinService },
{ provide: LightningService, useValue: lightningService },
{ provide: SparkService, useValue: sparkService },
{ provide: MoneroService, useValue: moneroService },
{ provide: ZanoService, useValue: zanoService },
{ provide: SolanaService, useValue: solanaService },
{ provide: TronService, useValue: tronService },
{ provide: CardanoService, useValue: cardanoService },
{ provide: ArweaveService, useValue: arweaveService },
{ provide: RailgunService, useValue: railgunService },
{ provide: BitcoinService, useValue: createMock<BitcoinService>() },
{ provide: LightningService, useValue: createMock<LightningService>() },
{ provide: SparkService, useValue: createMock<SparkService>() },
{ provide: MoneroService, useValue: createMock<MoneroService>() },
{ provide: ZanoService, useValue: createMock<ZanoService>() },
{ provide: SolanaService, useValue: createMock<SolanaService>() },
{ provide: TronService, useValue: createMock<TronService>() },
{ provide: CardanoService, useValue: createMock<CardanoService>() },
{ provide: ArweaveService, useValue: createMock<ArweaveService>() },
{ provide: RailgunService, useValue: createMock<RailgunService>() },
{ provide: BlockchainRegistryService, useValue: createMock<BlockchainRegistryService>() },
TestUtil.provideConfig(),
],
}).compile();

service = module.get<CryptoService>(CryptoService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});

it('should return Blockchain.BITCOIN for address bc1q4mzpjac5e53dmgnq54j58klvldhme39ed71234', () => {
Expand Down
26 changes: 26 additions & 0 deletions src/integration/blockchain/shared/evm/abi/erc1271.abi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[
{
"inputs": [
{
"internalType": "bytes32",
"name": "hash",
"type": "bytes32"
},
{
"internalType": "bytes",
"name": "signature",
"type": "bytes"
}
],
"name": "isValidSignature",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "view",
"type": "function"
}
]
16 changes: 16 additions & 0 deletions src/integration/blockchain/shared/evm/evm-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { FeeAmount, MethodParameters, Pool, Route, SwapQuoter, Trade } from '@un
import { AssetTransfersCategory, AssetTransfersWithMetadataResult, BigNumberish } from 'alchemy-sdk';
import BigNumber from 'bignumber.js';
import { Contract, BigNumber as EthersNumber, ethers } from 'ethers';
import { hashMessage } from 'ethers/lib/utils';
import { AlchemyService, AssetTransfersParams } from 'src/integration/alchemy/services/alchemy.service';
import ERC1271_ABI from 'src/integration/blockchain/shared/evm/abi/erc1271.abi.json';
import ERC20_ABI from 'src/integration/blockchain/shared/evm/abi/erc20.abi.json';
import SIGNATURE_TRANSFER_ABI from 'src/integration/blockchain/shared/evm/abi/signature-transfer.abi.json';
import UNISWAP_V3_NFT_MANAGER_ABI from 'src/integration/blockchain/shared/evm/abi/uniswap-v3-nft-manager.abi.json';
Expand Down Expand Up @@ -398,6 +400,20 @@ export abstract class EvmClient extends BlockchainClient {
return this.provider.getTransactionReceipt(txHash);
}

async isContract(address: string): Promise<boolean> {
const code = await this.provider.getCode(address);
return code !== '0x';
}

async verifyErc1271Signature(message: string, address: string, signature: string): Promise<boolean> {
const ERC1271_MAGIC_VALUE = '0x1626ba7e';

const hash = hashMessage(message);
const contract = new Contract(address, ERC1271_ABI, this.provider);
const result = await contract.isValidSignature(hash, signature);
return result === ERC1271_MAGIC_VALUE;
}

// got from https://gist.github.com/gluk64/fdea559472d957f1138ed93bcbc6f78a
async getTxError(txHash: string): Promise<string> {
const tx = await this.getTx(txHash);
Expand Down
69 changes: 52 additions & 17 deletions src/integration/blockchain/shared/services/crypto.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,18 @@ import { Asset } from 'src/shared/models/asset/asset.entity';
import { UserAddressType } from 'src/subdomains/generic/user/models/user/user.enum';
import { ArweaveService } from '../../arweave/services/arweave.service';
import { BitcoinService } from '../../bitcoin/node/bitcoin.service';
import { CardanoService } from '../../cardano/services/cardano.service';
import { LiquidHelper } from '../../liquid/liquid-helper';
import { MoneroService } from '../../monero/services/monero.service';
import { SolanaService } from '../../solana/services/solana.service';
import { SparkService } from '../../spark/spark.service';
import { TronService } from '../../tron/services/tron.service';
import { CardanoService } from '../../cardano/services/cardano.service';
import { ZanoService } from '../../zano/services/zano.service';
import { Blockchain } from '../enums/blockchain.enum';
import { EvmUtil } from '../evm/evm.util';
import { SignatureException } from '../exceptions/signature.exception';
import { EvmBlockchains, TestBlockchains } from '../util/blockchain.util';
import { BlockchainRegistryService } from './blockchain-registry.service';

@Injectable()
export class CryptoService {
Expand All @@ -38,6 +39,7 @@ export class CryptoService {
private readonly cardanoService: CardanoService,
private readonly arweaveService: ArweaveService,
private readonly railgunService: RailgunService,
private readonly blockchainRegistry: BlockchainRegistryService,
) {}

// --- PAYMENT REQUEST --- //
Expand Down Expand Up @@ -232,33 +234,66 @@ export class CryptoService {
}

// --- SIGNATURE VERIFICATION --- //
public async verifySignature(message: string, address: string, signature: string, key?: string): Promise<boolean> {
const blockchain = CryptoService.getDefaultBlockchainBasedOn(address);
public async verifySignature(
message: string,
address: string,
signature: string,
key?: string,
blockchain?: Blockchain,
): Promise<boolean> {
const detectedBlockchain = CryptoService.getDefaultBlockchainBasedOn(address);

try {
if (EvmBlockchains.includes(blockchain)) return this.verifyEthereumBased(message, address, signature);
if (blockchain === Blockchain.BITCOIN) return this.verifyBitcoinBased(message, address, signature, null);
if (blockchain === Blockchain.LIGHTNING) return await this.verifyLightning(address, message, signature);
if (blockchain === Blockchain.SPARK) return await this.verifySpark(message, address, signature);
if (blockchain === Blockchain.MONERO) return await this.verifyMonero(message, address, signature);
if (blockchain === Blockchain.ZANO) return await this.verifyZano(message, address, signature);
if (blockchain === Blockchain.SOLANA) return await this.verifySolana(message, address, signature);
if (blockchain === Blockchain.TRON) return await this.verifyTron(message, address, signature);
if (blockchain === Blockchain.LIQUID) return this.verifyLiquid(message, address, signature);
if (blockchain === Blockchain.ARWEAVE) return await this.verifyArweave(message, signature, key);
if (blockchain === Blockchain.CARDANO) return this.verifyCardano(message, address, signature, key);
if (blockchain === Blockchain.RAILGUN) return await this.verifyRailgun(message, address, signature);
if (EvmBlockchains.includes(detectedBlockchain))
return await this.verifyEthereumBased(message, address, signature, blockchain ?? detectedBlockchain);
if (detectedBlockchain === Blockchain.BITCOIN) return this.verifyBitcoinBased(message, address, signature, null);
if (detectedBlockchain === Blockchain.LIGHTNING) return await this.verifyLightning(address, message, signature);
if (detectedBlockchain === Blockchain.SPARK) return await this.verifySpark(message, address, signature);
if (detectedBlockchain === Blockchain.MONERO) return await this.verifyMonero(message, address, signature);
if (detectedBlockchain === Blockchain.ZANO) return await this.verifyZano(message, address, signature);
if (detectedBlockchain === Blockchain.SOLANA) return await this.verifySolana(message, address, signature);
if (detectedBlockchain === Blockchain.TRON) return await this.verifyTron(message, address, signature);
if (detectedBlockchain === Blockchain.LIQUID) return this.verifyLiquid(message, address, signature);
if (detectedBlockchain === Blockchain.ARWEAVE) return await this.verifyArweave(message, signature, key);
if (detectedBlockchain === Blockchain.CARDANO) return this.verifyCardano(message, address, signature, key);
if (detectedBlockchain === Blockchain.RAILGUN) return await this.verifyRailgun(message, address, signature);
} catch (e) {
if (e instanceof SignatureException) throw new BadRequestException(e.message);
}

return false;
}

private verifyEthereumBased(message: string, address: string, signature: string): boolean {
private async verifyEthereumBased(
message: string,
address: string,
signature: string,
blockchain: Blockchain,
): Promise<boolean> {
// there are signatures out there, which do not have '0x' in the beginning, but for verification this is needed
const signatureToUse = signature.startsWith('0x') ? signature : '0x' + signature;
return verifyMessage(message, signatureToUse).toLowerCase() === address.toLowerCase();

if (this.verifyEoaSignature(message, address, signatureToUse)) return true;

// Fallback to ERC-1271 for smart contract wallets
try {
const client = this.blockchainRegistry.getEvmClient(blockchain);
if (await client.isContract(address)) {
return await client.verifyErc1271Signature(message, address, signatureToUse);
}
} catch {
// ignore
}

return false;
}

private verifyEoaSignature(message: string, address: string, signature: string): boolean {
try {
return verifyMessage(message, signature).toLowerCase() === address.toLowerCase();
} catch {
return false;
}
}

private verifyBitcoinBased(message: string, address: string, signature: string, prefix: string | null): boolean {
Expand Down
4 changes: 2 additions & 2 deletions src/subdomains/core/buy-crypto/routes/buy/buy.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ export class BuyService {
return {
name: selector.userData.completeName,
street: address.street,
number: address.houseNumber,
...(address.houseNumber && { number: address.houseNumber }),
zip: address.zip,
city: address.city,
country: address.country?.name,
Expand All @@ -399,7 +399,7 @@ export class BuyService {
return {
name: selector.userData.completeName,
street: address.street,
number: address.houseNumber,
...(address.houseNumber && { number: address.houseNumber }),
zip: address.zip,
city: address.city,
country: address.country?.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export class BankInfoDto {
@ApiProperty()
street: string;

@ApiProperty()
number: string;
@ApiPropertyOptional()
number?: string;

@ApiProperty()
zip: string;
Expand Down
Loading
Loading