Skip to content

Commit 6d95fb7

Browse files
committed
Extracted chia specific logic into the infrastructure layer and kept application blockchain agnostic
Signed-off-by: Robert Gogete <[email protected]>
1 parent ca21a15 commit 6d95fb7

File tree

8 files changed

+216
-65
lines changed

8 files changed

+216
-65
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,34 @@
11
import { Block } from "../types/Block";
2+
import type { Coin, Peer, UnspentCoinsResponse } from '@dignetwork/datalayer-driver';
23

34

45
export interface IBlockchainService {
56
getCurrentBlockchainHeight(): Promise<number>;
67
getBlockchainBlockByHeight(height: number): Promise<Block>;
8+
// Key and address methods
9+
masterSecretKeyFromSeed(seed: Buffer): Buffer;
10+
secretKeyToPublicKey(secretKey: Buffer): Buffer;
11+
masterPublicKeyToWalletSyntheticKey(publicKey: Buffer): Buffer;
12+
masterSecretKeyToWalletSyntheticSecretKey(secretKey: Buffer): Buffer;
13+
masterPublicKeyToFirstPuzzleHash(publicKey: Buffer): Buffer;
14+
puzzleHashToAddress(puzzleHash: Buffer, prefix: string): string;
15+
signMessage(message: Buffer, privateKey: Buffer): Buffer;
16+
// Coin selection
17+
getCoinId(coin: Coin): Buffer;
18+
selectCoins(coins: Coin[], amount: bigint): Coin[];
19+
// ColdWallet/WalletService methods
20+
getPuzzleHash(address: string): Buffer;
21+
verifyKeySignature(signature: Buffer, publicKey: Buffer, message: Buffer): boolean;
22+
listUnspentCoins(
23+
peer: Peer,
24+
puzzleHash: Buffer,
25+
previousHeight: number,
26+
previousHeaderHash: Buffer
27+
): Promise<UnspentCoinsResponse>;
28+
isCoinSpendable(
29+
peer: Peer,
30+
coinId: Buffer,
31+
lastHeight: number,
32+
headerHash: Buffer
33+
): Promise<boolean>;
734
}

src/application/services/WalletService.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ import * as bip39 from 'bip39';
33
import { NconfService } from '../../infrastructure/ConfigurationServices/NconfService';
44
import { EncryptionService } from './EncryptionService';
55
import { EncryptedData } from '../types/EncryptedData';
6-
import { Peer, verifySignedMessage } from '@dignetwork/datalayer-driver';
76
import { Wallet } from '../types/Wallet';
7+
import type { IBlockchainService } from '../interfaces/IBlockChainService';
8+
import { ChiaBlockchainService } from '../../infrastructure/BlockchainServices/ChiaBlockchainService';
9+
import { Peer } from '@dignetwork/datalayer-driver';
810

911
const KEYRING_FILE = 'keyring.json';
1012

1113
export class WalletService {
14+
private static blockchain: IBlockchainService = new ChiaBlockchainService();
15+
1216
public static async loadWallet(walletName: string = 'default'): Promise<Wallet> {
1317
const mnemonic = await this.getMnemonicFromKeyring(walletName);
1418
if (mnemonic) return new Wallet(mnemonic);
@@ -46,7 +50,7 @@ export class WalletService {
4650
publicKey: string,
4751
): Promise<boolean> {
4852
const message = `Signing this message to prove ownership of key.\n\nNonce: ${nonce}`;
49-
return verifySignedMessage(
53+
return this.blockchain.verifyKeySignature(
5054
Buffer.from(signature, 'hex'),
5155
Buffer.from(publicKey, 'hex'),
5256
Buffer.from(message, 'utf-8'),
@@ -59,7 +63,7 @@ export class WalletService {
5963

6064
public static async isCoinSpendable(peer: Peer, coinId: Buffer, lastHeight: number, lastHeaderHash: string): Promise<boolean> {
6165
try {
62-
return await peer.isCoinSpent(coinId, lastHeight, Buffer.from(lastHeaderHash, 'hex'));
66+
return await this.blockchain.isCoinSpendable(peer, coinId, lastHeight, Buffer.from(lastHeaderHash, 'hex'));
6367
} catch {
6468
return false;
6569
}

src/application/types/ColdWallet.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,37 @@
1-
import { Peer, UnspentCoinsResponse, addressToPuzzleHash, verifySignedMessage, masterPublicKeyToWalletSyntheticKey, masterPublicKeyToFirstPuzzleHash } from '@dignetwork/datalayer-driver';
1+
import type { Peer, UnspentCoinsResponse } from '@dignetwork/datalayer-driver';
2+
import type { IBlockchainService } from '../interfaces/IBlockChainService';
23

34
export interface IColdWallet {
45
getPuzzleHash(address: string): Buffer;
56
verifyKeySignature(signature: Buffer, publicKey: Buffer, message: Buffer): boolean;
67
listUnspentCoins(
78
peer: Peer,
89
puzzleHash: Buffer,
9-
previousHeight: number | undefined | null,
10+
previousHeight: number,
1011
previousHeaderHash: Buffer
1112
): Promise<UnspentCoinsResponse>;
1213
isCoinSpendable(
1314
peer: Peer,
1415
coinId: Buffer,
15-
lastHeight: number | undefined | null,
16+
lastHeight: number,
1617
headerHash: Buffer
1718
): Promise<boolean>;
1819
masterPublicKeyToWalletSyntheticKey(publicKey: Buffer): Buffer;
1920
masterPublicKeyToFirstPuzzleHash(publicKey: Buffer): Buffer;
2021
}
2122

2223
export class ColdWallet implements IColdWallet {
24+
private blockchain: IBlockchainService;
25+
constructor(blockchain: IBlockchainService) {
26+
this.blockchain = blockchain;
27+
}
28+
2329
getPuzzleHash(address: string): Buffer {
24-
return addressToPuzzleHash(address);
30+
return this.blockchain.getPuzzleHash(address);
2531
}
2632

2733
verifyKeySignature(signature: Buffer, publicKey: Buffer, message: Buffer): boolean {
28-
return verifySignedMessage(signature, publicKey, message);
34+
return this.blockchain.verifyKeySignature(signature, publicKey, message);
2935
}
3036

3137
async listUnspentCoins(
@@ -34,7 +40,7 @@ export class ColdWallet implements IColdWallet {
3440
previousHeight: number,
3541
previousHeaderHash: Buffer
3642
) {
37-
return await peer.getAllUnspentCoins(puzzleHash, previousHeight, previousHeaderHash);
43+
return await this.blockchain.listUnspentCoins(peer, puzzleHash, previousHeight, previousHeaderHash);
3844
}
3945

4046
async isCoinSpendable(
@@ -43,14 +49,14 @@ export class ColdWallet implements IColdWallet {
4349
lastHeight: number,
4450
headerHash: Buffer
4551
): Promise<boolean> {
46-
return !(await peer.isCoinSpent(coinId, lastHeight, headerHash));
52+
return await this.blockchain.isCoinSpendable(peer, coinId, lastHeight, headerHash);
4753
}
4854

4955
masterPublicKeyToWalletSyntheticKey(publicKey: Buffer): Buffer {
50-
return masterPublicKeyToWalletSyntheticKey(publicKey);
56+
return this.blockchain.masterPublicKeyToWalletSyntheticKey(publicKey);
5157
}
5258

5359
masterPublicKeyToFirstPuzzleHash(publicKey: Buffer): Buffer {
54-
return masterPublicKeyToFirstPuzzleHash(publicKey);
60+
return this.blockchain.masterPublicKeyToFirstPuzzleHash(publicKey);
5561
}
5662
}

src/application/types/Wallet.ts

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,8 @@
1-
import {
2-
Coin,
3-
getCoinId,
4-
masterPublicKeyToFirstPuzzleHash,
5-
masterPublicKeyToWalletSyntheticKey,
6-
masterSecretKeyToWalletSyntheticSecretKey,
7-
Peer,
8-
puzzleHashToAddress,
9-
secretKeyToPublicKey,
10-
selectCoins,
11-
signMessage,
12-
} from '@dignetwork/datalayer-driver';
131
import { mnemonicToSeedSync } from 'bip39';
14-
import { PrivateKey } from 'chia-bls';
152
import { FileCacheService } from '../services/FileCacheService';
3+
import type { IBlockchainService } from '../interfaces/IBlockChainService';
4+
import { Coin, Peer } from '@dignetwork/datalayer-driver';
5+
import { ChiaBlockchainService } from '../../infrastructure/BlockchainServices/ChiaBlockchainService';
166

177
const COIN_CACHE_DURATION = 600000;
188

@@ -36,9 +26,11 @@ export interface IWallet {
3626

3727
export class Wallet implements IWallet {
3828
private mnemonic: string;
29+
private blockchain: IBlockchainService;
3930

4031
public constructor(mnemonic: string) {
4132
this.mnemonic = mnemonic;
33+
this.blockchain = new ChiaBlockchainService();
4234
}
4335

4436
public getMnemonic(): string {
@@ -50,35 +42,35 @@ export class Wallet implements IWallet {
5042

5143
public async getMasterSecretKey(): Promise<Buffer> {
5244
const seed = mnemonicToSeedSync(this.getMnemonic());
53-
return Buffer.from(PrivateKey.fromSeed(seed).toHex(), 'hex');
45+
return this.blockchain.masterSecretKeyFromSeed(seed);
5446
}
5547

5648
public async getPublicSyntheticKey(): Promise<Buffer> {
5749
const master_sk = await this.getMasterSecretKey();
58-
const master_pk = secretKeyToPublicKey(master_sk);
59-
return masterPublicKeyToWalletSyntheticKey(master_pk);
50+
const master_pk = this.blockchain.secretKeyToPublicKey(master_sk);
51+
return this.blockchain.masterPublicKeyToWalletSyntheticKey(master_pk);
6052
}
6153

6254
public async getPrivateSyntheticKey(): Promise<Buffer> {
6355
const master_sk = await this.getMasterSecretKey();
64-
return masterSecretKeyToWalletSyntheticSecretKey(master_sk);
56+
return this.blockchain.masterSecretKeyToWalletSyntheticSecretKey(master_sk);
6557
}
6658

6759
public async getOwnerPuzzleHash(): Promise<Buffer> {
6860
const master_sk = await this.getMasterSecretKey();
69-
const master_pk = secretKeyToPublicKey(master_sk);
70-
return masterPublicKeyToFirstPuzzleHash(master_pk);
61+
const master_pk = this.blockchain.secretKeyToPublicKey(master_sk);
62+
return this.blockchain.masterPublicKeyToFirstPuzzleHash(master_pk);
7163
}
7264

7365
public async getOwnerPublicKey(): Promise<string> {
7466
const ownerPuzzleHash = await this.getOwnerPuzzleHash();
75-
return puzzleHashToAddress(ownerPuzzleHash, 'xch'); // might need to also add txch for testnet
67+
return this.blockchain.puzzleHashToAddress(ownerPuzzleHash, 'xch');
7668
}
7769

7870
public async createKeyOwnershipSignature(nonce: string): Promise<string> {
7971
const message = `Signing this message to prove ownership of key.\n\nNonce: ${nonce}`;
8072
const privateSyntheticKey = await this.getPrivateSyntheticKey();
81-
const signature = signMessage(Buffer.from(message, 'utf-8'), privateSyntheticKey);
73+
const signature = this.blockchain.signMessage(Buffer.from(message, 'utf-8'), privateSyntheticKey);
8274
return signature.toString('hex');
8375
}
8476

@@ -97,7 +89,7 @@ export class Wallet implements IWallet {
9789
// Define a function to attempt selecting unspent coins
9890
const trySelectCoins = async (): Promise<Coin[]> => {
9991
const now = Date.now();
100-
const omitCoinIds = omitCoins.map((coin) => getCoinId(coin).toString('hex'));
92+
const omitCoinIds = omitCoins.map((coin) => this.blockchain.getCoinId(coin).toString('hex'));
10193

10294
// Update omitCoinIds with currently valid reserved coins
10395
const cachedReservedCoins = cache.getCachedKeys();
@@ -120,10 +112,10 @@ export class Wallet implements IWallet {
120112
);
121113

122114
const unspentCoins = coinsResp.coins.filter(
123-
(coin) => !omitCoinIds.includes(getCoinId(coin).toString('hex')),
115+
(coin) => !omitCoinIds.includes(this.blockchain.getCoinId(coin).toString('hex')),
124116
);
125117

126-
const selectedCoins = selectCoins(unspentCoins, feeBigInt + coinAmount);
118+
const selectedCoins = this.blockchain.selectCoins(unspentCoins, feeBigInt + coinAmount);
127119

128120
return selectedCoins;
129121
};
@@ -157,7 +149,7 @@ export class Wallet implements IWallet {
157149

158150
// Reserve the selected coins
159151
selectedCoins.forEach((coin) => {
160-
const coinId = getCoinId(coin).toString('hex');
152+
const coinId = this.blockchain.getCoinId(coin).toString('hex');
161153
cache.set(coinId, { coinId, expiry: Date.now() + COIN_CACHE_DURATION });
162154
});
163155

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,87 @@
1-
21
import { IBlockchainService } from "../../application/interfaces/IBlockChainService";
32
import { Block } from "../../application/types/Block";
3+
import {
4+
masterSecretKeyToWalletSyntheticSecretKey,
5+
masterPublicKeyToWalletSyntheticKey,
6+
masterPublicKeyToFirstPuzzleHash,
7+
secretKeyToPublicKey,
8+
puzzleHashToAddress,
9+
signMessage,
10+
getCoinId,
11+
selectCoins,
12+
addressToPuzzleHash,
13+
verifySignedMessage,
14+
} from '@dignetwork/datalayer-driver';
15+
import type { Coin, Peer, UnspentCoinsResponse } from '@dignetwork/datalayer-driver';
16+
import { PrivateKey } from 'chia-bls';
417

518
export class ChiaBlockchainService implements IBlockchainService {
619
async getCurrentBlockchainHeight(): Promise<number> {
720
return 0;
821
}
22+
923
async getBlockchainBlockByHeight(height: number): Promise<Block> {
1024
return { hash: Buffer.from('abc', 'hex'), blockHeight: height };
1125
}
26+
27+
masterSecretKeyFromSeed(seed: Buffer): Buffer {
28+
return Buffer.from(PrivateKey.fromSeed(seed).toHex(), 'hex');
29+
}
30+
31+
secretKeyToPublicKey(secretKey: Buffer): Buffer {
32+
return secretKeyToPublicKey(secretKey);
33+
}
34+
35+
masterPublicKeyToWalletSyntheticKey(publicKey: Buffer): Buffer {
36+
return masterPublicKeyToWalletSyntheticKey(publicKey);
37+
}
38+
39+
masterSecretKeyToWalletSyntheticSecretKey(secretKey: Buffer): Buffer {
40+
return masterSecretKeyToWalletSyntheticSecretKey(secretKey);
41+
}
42+
43+
masterPublicKeyToFirstPuzzleHash(publicKey: Buffer): Buffer {
44+
return masterPublicKeyToFirstPuzzleHash(publicKey);
45+
}
46+
47+
puzzleHashToAddress(puzzleHash: Buffer, prefix: string): string {
48+
return puzzleHashToAddress(puzzleHash, prefix);
49+
}
50+
51+
signMessage(message: Buffer, privateKey: Buffer): Buffer {
52+
return signMessage(message, privateKey);
53+
}
54+
55+
getCoinId(coin: Coin): Buffer {
56+
return getCoinId(coin);
57+
}
58+
59+
selectCoins(coins: Coin[], amount: bigint): Coin[] {
60+
return selectCoins(coins, amount);
61+
}
62+
63+
getPuzzleHash(address: string): Buffer {
64+
return addressToPuzzleHash(address);
65+
}
66+
verifyKeySignature(signature: Buffer, publicKey: Buffer, message: Buffer): boolean {
67+
return verifySignedMessage(signature, publicKey, message);
68+
}
69+
70+
async listUnspentCoins(
71+
peer: Peer,
72+
puzzleHash: Buffer,
73+
previousHeight: number,
74+
previousHeaderHash: Buffer
75+
): Promise<UnspentCoinsResponse> {
76+
return await peer.getAllUnspentCoins(puzzleHash, previousHeight, previousHeaderHash);
77+
}
78+
79+
async isCoinSpendable(
80+
peer: Peer,
81+
coinId: Buffer,
82+
lastHeight: number,
83+
headerHash: Buffer
84+
): Promise<boolean> {
85+
return !(await peer.isCoinSpent(coinId, lastHeight, headerHash));
86+
}
1287
}

src/infrastructure/BlockchainServices/TestBlockchainService.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
22
import { IBlockchainService } from "../../application/interfaces/IBlockChainService";
33
import { Block } from "../../application/types/Block";
44
import Database from 'better-sqlite3';
5+
import type { Coin, Peer, UnspentCoinsResponse } from '@dignetwork/datalayer-driver';
56

67
export class TestBlockchainService implements IBlockchainService {
78
private db: Database.Database;
@@ -24,4 +25,39 @@ export class TestBlockchainService implements IBlockchainService {
2425
blockHeight: row.blockHeight,
2526
};
2627
}
28+
29+
masterSecretKeyFromSeed(seed: Buffer): Buffer { return Buffer.alloc(32, 1); }
30+
secretKeyToPublicKey(secretKey: Buffer): Buffer { return Buffer.alloc(32, 2); }
31+
masterPublicKeyToWalletSyntheticKey(publicKey: Buffer): Buffer { return Buffer.alloc(32, 3); }
32+
masterSecretKeyToWalletSyntheticSecretKey(secretKey: Buffer): Buffer { return Buffer.alloc(32, 4); }
33+
masterPublicKeyToFirstPuzzleHash(publicKey: Buffer): Buffer { return Buffer.alloc(32, 5); }
34+
puzzleHashToAddress(puzzleHash: Buffer, prefix: string): string { return prefix + puzzleHash.toString('hex'); }
35+
signMessage(message: Buffer, privateKey: Buffer): Buffer { return Buffer.from('deadbeef', 'hex'); }
36+
getCoinId(coin: Coin): Buffer { return Buffer.from('cafebabe', 'hex'); }
37+
selectCoins(coins: Coin[], amount: bigint): Coin[] { return coins.slice(0, 1); }
38+
39+
// New methods for ColdWallet/WalletService
40+
getPuzzleHash(address: string): Buffer {
41+
return Buffer.from(address, 'utf-8'); // Dummy
42+
}
43+
verifyKeySignature(signature: Buffer, publicKey: Buffer, message: Buffer): boolean {
44+
return true; // Dummy
45+
}
46+
async listUnspentCoins(
47+
peer: Peer,
48+
puzzleHash: Buffer,
49+
previousHeight: number,
50+
previousHeaderHash: Buffer
51+
): Promise<UnspentCoinsResponse> {
52+
// Dummy: return a fake response
53+
return { coins: [], lastHeight: 0, lastHeaderHash: Buffer.alloc(32) };
54+
}
55+
async isCoinSpendable(
56+
peer: Peer,
57+
coinId: Buffer,
58+
lastHeight: number,
59+
headerHash: Buffer
60+
): Promise<boolean> {
61+
return true; // Dummy
62+
}
2763
}

0 commit comments

Comments
 (0)