Skip to content

Commit 42e7c85

Browse files
authored
Merge pull request #2936 from DFXswiss/develop
2 parents ed9be14 + d4b34fd commit 42e7c85

34 files changed

Lines changed: 781 additions & 126 deletions

.github/workflows/pr-review-bot.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,18 @@ jobs:
5555
continue-on-error: true
5656
run: |
5757
npm run lint 2>&1 | tee eslint-output.txt || true
58-
WARNINGS=$(grep -c "warning" eslint-output.txt || echo "0")
59-
ERRORS=$(grep -c "error" eslint-output.txt || echo "0")
60-
echo "warnings=$WARNINGS" >> $GITHUB_OUTPUT
61-
echo "errors=$ERRORS" >> $GITHUB_OUTPUT
58+
WARNINGS=$(grep -c "warning" eslint-output.txt || true)
59+
ERRORS=$(grep -c "error" eslint-output.txt || true)
60+
echo "warnings=${WARNINGS:-0}" >> $GITHUB_OUTPUT
61+
echo "errors=${ERRORS:-0}" >> $GITHUB_OUTPUT
6262
6363
- name: Run TypeScript check
6464
id: typescript
6565
continue-on-error: true
6666
run: |
6767
npx tsc --noEmit 2>&1 | tee tsc-output.txt || true
68-
ERRORS=$(grep -c "error TS" tsc-output.txt || echo "0")
69-
echo "errors=$ERRORS" >> $GITHUB_OUTPUT
68+
ERRORS=$(grep -c "error TS" tsc-output.txt || true)
69+
echo "errors=${ERRORS:-0}" >> $GITHUB_OUTPUT
7070
7171
- name: Security Audit
7272
id: audit
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
3+
* @typedef {import('typeorm').QueryRunner} QueryRunner
4+
*/
5+
6+
/**
7+
* @class
8+
* @implements {MigrationInterface}
9+
*/
10+
module.exports = class AddCustodyAccountTables1768341824012 {
11+
name = 'AddCustodyAccountTables1768341824012'
12+
13+
/**
14+
* @param {QueryRunner} queryRunner
15+
*/
16+
async up(queryRunner) {
17+
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"))`);
18+
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_380e225bfd7707fff0e4f98035" ON "custody_account_access" ("accountId", "userDataId") `);
19+
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"))`);
20+
await queryRunner.query(`ALTER TABLE "custody_order" ADD "accountId" int`);
21+
await queryRunner.query(`ALTER TABLE "custody_order" ADD "initiatedById" int`);
22+
await queryRunner.query(`ALTER TABLE "custody_balance" ADD "accountId" int`);
23+
await queryRunner.query(`ALTER TABLE "user" ADD "custodyAccountId" int`);
24+
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`);
25+
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`);
26+
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`);
27+
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`);
28+
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`);
29+
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`);
30+
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`);
31+
}
32+
33+
/**
34+
* @param {QueryRunner} queryRunner
35+
*/
36+
async down(queryRunner) {
37+
await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_bf8ce326ec41adc02940bccf91a"`);
38+
await queryRunner.query(`ALTER TABLE "custody_balance" DROP CONSTRAINT "FK_b141d5e0d74c87aef92eae2847a"`);
39+
await queryRunner.query(`ALTER TABLE "custody_order" DROP CONSTRAINT "FK_67425e623d89efe4ae1a48dbad6"`);
40+
await queryRunner.query(`ALTER TABLE "custody_order" DROP CONSTRAINT "FK_6a769cd0d90bc68cafdd533f03e"`);
41+
await queryRunner.query(`ALTER TABLE "custody_account" DROP CONSTRAINT "FK_b89a7cbab6c121f5a092815fce3"`);
42+
await queryRunner.query(`ALTER TABLE "custody_account_access" DROP CONSTRAINT "FK_8a4612269b283bf40950ddb8485"`);
43+
await queryRunner.query(`ALTER TABLE "custody_account_access" DROP CONSTRAINT "FK_45213c9c7521d41be00fa5ead93"`);
44+
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "custodyAccountId"`);
45+
await queryRunner.query(`ALTER TABLE "custody_balance" DROP COLUMN "accountId"`);
46+
await queryRunner.query(`ALTER TABLE "custody_order" DROP COLUMN "initiatedById"`);
47+
await queryRunner.query(`ALTER TABLE "custody_order" DROP COLUMN "accountId"`);
48+
await queryRunner.query(`DROP TABLE "custody_account"`);
49+
await queryRunner.query(`DROP INDEX "IDX_380e225bfd7707fff0e4f98035" ON "custody_account_access"`);
50+
await queryRunner.query(`DROP TABLE "custody_account_access"`);
51+
}
52+
}

src/integration/blockchain/bitcoin/services/__tests__/crypto.service.spec.ts

Lines changed: 14 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { createMock } from '@golevelup/ts-jest';
2-
import { Test, TestingModule } from '@nestjs/testing';
2+
import { Test } from '@nestjs/testing';
33
import { ArweaveService } from 'src/integration/blockchain/arweave/services/arweave.service';
44
import { CardanoService } from 'src/integration/blockchain/cardano/services/cardano.service';
55
import { MoneroService } from 'src/integration/blockchain/monero/services/monero.service';
66
import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum';
7+
import { BlockchainRegistryService } from 'src/integration/blockchain/shared/services/blockchain-registry.service';
78
import { CryptoService } from 'src/integration/blockchain/shared/services/crypto.service';
89
import { SolanaService } from 'src/integration/blockchain/solana/services/solana.service';
910
import { SparkService } from 'src/integration/blockchain/spark/spark.service';
@@ -16,53 +17,24 @@ import { UserAddressType } from 'src/subdomains/generic/user/models/user/user.en
1617
import { BitcoinService } from '../../node/bitcoin.service';
1718

1819
describe('CryptoService', () => {
19-
let service: CryptoService;
20-
21-
let bitcoinService: BitcoinService;
22-
let lightningService: LightningService;
23-
let sparkService: SparkService;
24-
let moneroService: MoneroService;
25-
let zanoService: ZanoService;
26-
let solanaService: SolanaService;
27-
let tronService: TronService;
28-
let cardanoService: CardanoService;
29-
let arweaveService: ArweaveService;
30-
let railgunService: RailgunService;
31-
3220
beforeEach(async () => {
33-
bitcoinService = createMock<BitcoinService>();
34-
lightningService = createMock<LightningService>();
35-
sparkService = createMock<SparkService>();
36-
moneroService = createMock<MoneroService>();
37-
zanoService = createMock<ZanoService>();
38-
solanaService = createMock<SolanaService>();
39-
tronService = createMock<TronService>();
40-
cardanoService = createMock<CardanoService>();
41-
arweaveService = createMock<ArweaveService>();
42-
railgunService = createMock<RailgunService>();
43-
44-
const module: TestingModule = await Test.createTestingModule({
21+
await Test.createTestingModule({
4522
providers: [
4623
CryptoService,
47-
{ provide: BitcoinService, useValue: bitcoinService },
48-
{ provide: LightningService, useValue: lightningService },
49-
{ provide: SparkService, useValue: sparkService },
50-
{ provide: MoneroService, useValue: moneroService },
51-
{ provide: ZanoService, useValue: zanoService },
52-
{ provide: SolanaService, useValue: solanaService },
53-
{ provide: TronService, useValue: tronService },
54-
{ provide: CardanoService, useValue: cardanoService },
55-
{ provide: ArweaveService, useValue: arweaveService },
56-
{ provide: RailgunService, useValue: railgunService },
24+
{ provide: BitcoinService, useValue: createMock<BitcoinService>() },
25+
{ provide: LightningService, useValue: createMock<LightningService>() },
26+
{ provide: SparkService, useValue: createMock<SparkService>() },
27+
{ provide: MoneroService, useValue: createMock<MoneroService>() },
28+
{ provide: ZanoService, useValue: createMock<ZanoService>() },
29+
{ provide: SolanaService, useValue: createMock<SolanaService>() },
30+
{ provide: TronService, useValue: createMock<TronService>() },
31+
{ provide: CardanoService, useValue: createMock<CardanoService>() },
32+
{ provide: ArweaveService, useValue: createMock<ArweaveService>() },
33+
{ provide: RailgunService, useValue: createMock<RailgunService>() },
34+
{ provide: BlockchainRegistryService, useValue: createMock<BlockchainRegistryService>() },
5735
TestUtil.provideConfig(),
5836
],
5937
}).compile();
60-
61-
service = module.get<CryptoService>(CryptoService);
62-
});
63-
64-
it('should be defined', () => {
65-
expect(service).toBeDefined();
6638
});
6739

6840
it('should return Blockchain.BITCOIN for address bc1q4mzpjac5e53dmgnq54j58klvldhme39ed71234', () => {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[
2+
{
3+
"inputs": [
4+
{
5+
"internalType": "bytes32",
6+
"name": "hash",
7+
"type": "bytes32"
8+
},
9+
{
10+
"internalType": "bytes",
11+
"name": "signature",
12+
"type": "bytes"
13+
}
14+
],
15+
"name": "isValidSignature",
16+
"outputs": [
17+
{
18+
"internalType": "bytes4",
19+
"name": "",
20+
"type": "bytes4"
21+
}
22+
],
23+
"stateMutability": "view",
24+
"type": "function"
25+
}
26+
]

src/integration/blockchain/shared/evm/evm-client.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { FeeAmount, MethodParameters, Pool, Route, SwapQuoter, Trade } from '@un
77
import { AssetTransfersCategory, AssetTransfersWithMetadataResult, BigNumberish } from 'alchemy-sdk';
88
import BigNumber from 'bignumber.js';
99
import { Contract, BigNumber as EthersNumber, ethers } from 'ethers';
10+
import { hashMessage } from 'ethers/lib/utils';
1011
import { AlchemyService, AssetTransfersParams } from 'src/integration/alchemy/services/alchemy.service';
12+
import ERC1271_ABI from 'src/integration/blockchain/shared/evm/abi/erc1271.abi.json';
1113
import ERC20_ABI from 'src/integration/blockchain/shared/evm/abi/erc20.abi.json';
1214
import SIGNATURE_TRANSFER_ABI from 'src/integration/blockchain/shared/evm/abi/signature-transfer.abi.json';
1315
import UNISWAP_V3_NFT_MANAGER_ABI from 'src/integration/blockchain/shared/evm/abi/uniswap-v3-nft-manager.abi.json';
@@ -398,6 +400,20 @@ export abstract class EvmClient extends BlockchainClient {
398400
return this.provider.getTransactionReceipt(txHash);
399401
}
400402

403+
async isContract(address: string): Promise<boolean> {
404+
const code = await this.provider.getCode(address);
405+
return code !== '0x';
406+
}
407+
408+
async verifyErc1271Signature(message: string, address: string, signature: string): Promise<boolean> {
409+
const ERC1271_MAGIC_VALUE = '0x1626ba7e';
410+
411+
const hash = hashMessage(message);
412+
const contract = new Contract(address, ERC1271_ABI, this.provider);
413+
const result = await contract.isValidSignature(hash, signature);
414+
return result === ERC1271_MAGIC_VALUE;
415+
}
416+
401417
// got from https://gist.github.com/gluk64/fdea559472d957f1138ed93bcbc6f78a
402418
async getTxError(txHash: string): Promise<string> {
403419
const tx = await this.getTx(txHash);

src/integration/blockchain/shared/services/crypto.service.ts

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@ import { Asset } from 'src/shared/models/asset/asset.entity';
1111
import { UserAddressType } from 'src/subdomains/generic/user/models/user/user.enum';
1212
import { ArweaveService } from '../../arweave/services/arweave.service';
1313
import { BitcoinService } from '../../bitcoin/node/bitcoin.service';
14+
import { CardanoService } from '../../cardano/services/cardano.service';
1415
import { LiquidHelper } from '../../liquid/liquid-helper';
1516
import { MoneroService } from '../../monero/services/monero.service';
1617
import { SolanaService } from '../../solana/services/solana.service';
1718
import { SparkService } from '../../spark/spark.service';
1819
import { TronService } from '../../tron/services/tron.service';
19-
import { CardanoService } from '../../cardano/services/cardano.service';
2020
import { ZanoService } from '../../zano/services/zano.service';
2121
import { Blockchain } from '../enums/blockchain.enum';
2222
import { EvmUtil } from '../evm/evm.util';
2323
import { SignatureException } from '../exceptions/signature.exception';
2424
import { EvmBlockchains, TestBlockchains } from '../util/blockchain.util';
25+
import { BlockchainRegistryService } from './blockchain-registry.service';
2526

2627
@Injectable()
2728
export class CryptoService {
@@ -38,6 +39,7 @@ export class CryptoService {
3839
private readonly cardanoService: CardanoService,
3940
private readonly arweaveService: ArweaveService,
4041
private readonly railgunService: RailgunService,
42+
private readonly blockchainRegistry: BlockchainRegistryService,
4143
) {}
4244

4345
// --- PAYMENT REQUEST --- //
@@ -232,33 +234,66 @@ export class CryptoService {
232234
}
233235

234236
// --- SIGNATURE VERIFICATION --- //
235-
public async verifySignature(message: string, address: string, signature: string, key?: string): Promise<boolean> {
236-
const blockchain = CryptoService.getDefaultBlockchainBasedOn(address);
237+
public async verifySignature(
238+
message: string,
239+
address: string,
240+
signature: string,
241+
key?: string,
242+
blockchain?: Blockchain,
243+
): Promise<boolean> {
244+
const detectedBlockchain = CryptoService.getDefaultBlockchainBasedOn(address);
237245

238246
try {
239-
if (EvmBlockchains.includes(blockchain)) return this.verifyEthereumBased(message, address, signature);
240-
if (blockchain === Blockchain.BITCOIN) return this.verifyBitcoinBased(message, address, signature, null);
241-
if (blockchain === Blockchain.LIGHTNING) return await this.verifyLightning(address, message, signature);
242-
if (blockchain === Blockchain.SPARK) return await this.verifySpark(message, address, signature);
243-
if (blockchain === Blockchain.MONERO) return await this.verifyMonero(message, address, signature);
244-
if (blockchain === Blockchain.ZANO) return await this.verifyZano(message, address, signature);
245-
if (blockchain === Blockchain.SOLANA) return await this.verifySolana(message, address, signature);
246-
if (blockchain === Blockchain.TRON) return await this.verifyTron(message, address, signature);
247-
if (blockchain === Blockchain.LIQUID) return this.verifyLiquid(message, address, signature);
248-
if (blockchain === Blockchain.ARWEAVE) return await this.verifyArweave(message, signature, key);
249-
if (blockchain === Blockchain.CARDANO) return this.verifyCardano(message, address, signature, key);
250-
if (blockchain === Blockchain.RAILGUN) return await this.verifyRailgun(message, address, signature);
247+
if (EvmBlockchains.includes(detectedBlockchain))
248+
return await this.verifyEthereumBased(message, address, signature, blockchain ?? detectedBlockchain);
249+
if (detectedBlockchain === Blockchain.BITCOIN) return this.verifyBitcoinBased(message, address, signature, null);
250+
if (detectedBlockchain === Blockchain.LIGHTNING) return await this.verifyLightning(address, message, signature);
251+
if (detectedBlockchain === Blockchain.SPARK) return await this.verifySpark(message, address, signature);
252+
if (detectedBlockchain === Blockchain.MONERO) return await this.verifyMonero(message, address, signature);
253+
if (detectedBlockchain === Blockchain.ZANO) return await this.verifyZano(message, address, signature);
254+
if (detectedBlockchain === Blockchain.SOLANA) return await this.verifySolana(message, address, signature);
255+
if (detectedBlockchain === Blockchain.TRON) return await this.verifyTron(message, address, signature);
256+
if (detectedBlockchain === Blockchain.LIQUID) return this.verifyLiquid(message, address, signature);
257+
if (detectedBlockchain === Blockchain.ARWEAVE) return await this.verifyArweave(message, signature, key);
258+
if (detectedBlockchain === Blockchain.CARDANO) return this.verifyCardano(message, address, signature, key);
259+
if (detectedBlockchain === Blockchain.RAILGUN) return await this.verifyRailgun(message, address, signature);
251260
} catch (e) {
252261
if (e instanceof SignatureException) throw new BadRequestException(e.message);
253262
}
254263

255264
return false;
256265
}
257266

258-
private verifyEthereumBased(message: string, address: string, signature: string): boolean {
267+
private async verifyEthereumBased(
268+
message: string,
269+
address: string,
270+
signature: string,
271+
blockchain: Blockchain,
272+
): Promise<boolean> {
259273
// there are signatures out there, which do not have '0x' in the beginning, but for verification this is needed
260274
const signatureToUse = signature.startsWith('0x') ? signature : '0x' + signature;
261-
return verifyMessage(message, signatureToUse).toLowerCase() === address.toLowerCase();
275+
276+
if (this.verifyEoaSignature(message, address, signatureToUse)) return true;
277+
278+
// Fallback to ERC-1271 for smart contract wallets
279+
try {
280+
const client = this.blockchainRegistry.getEvmClient(blockchain);
281+
if (await client.isContract(address)) {
282+
return await client.verifyErc1271Signature(message, address, signatureToUse);
283+
}
284+
} catch {
285+
// ignore
286+
}
287+
288+
return false;
289+
}
290+
291+
private verifyEoaSignature(message: string, address: string, signature: string): boolean {
292+
try {
293+
return verifyMessage(message, signature).toLowerCase() === address.toLowerCase();
294+
} catch {
295+
return false;
296+
}
262297
}
263298

264299
private verifyBitcoinBased(message: string, address: string, signature: string, prefix: string | null): boolean {

src/subdomains/core/buy-crypto/routes/buy/buy.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ export class BuyService {
377377
return {
378378
name: selector.userData.completeName,
379379
street: address.street,
380-
number: address.houseNumber,
380+
...(address.houseNumber && { number: address.houseNumber }),
381381
zip: address.zip,
382382
city: address.city,
383383
country: address.country?.name,
@@ -399,7 +399,7 @@ export class BuyService {
399399
return {
400400
name: selector.userData.completeName,
401401
street: address.street,
402-
number: address.houseNumber,
402+
...(address.houseNumber && { number: address.houseNumber }),
403403
zip: address.zip,
404404
city: address.city,
405405
country: address.country?.name,

src/subdomains/core/buy-crypto/routes/buy/dto/buy-payment-info.dto.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ export class BankInfoDto {
1616
@ApiProperty()
1717
street: string;
1818

19-
@ApiProperty()
20-
number: string;
19+
@ApiPropertyOptional()
20+
number?: string;
2121

2222
@ApiProperty()
2323
zip: string;

0 commit comments

Comments
 (0)