feat: blockchain (#1408)

Co-authored-by: root <root@huahua.daoyoucloud.com>
Co-authored-by: bai.zixv <bai.zixv@foxmail.com>
Co-authored-by: sealday <zhanglin@daoyoucloud.com>
Co-authored-by: TomyJan <TomyJan6@gmail.com>
Co-authored-by: luliangqiang <2650321653@qq.com>
Co-authored-by: yoona <1486343814@qq.com>
Co-authored-by: wjh <wwwjh0710@163.com>
Reviewed-on: daoyoucloud/tachybase#1408
Reviewed-by: sealday <zhanglin@daoyoucloud.com>
Co-authored-by: hua <1494133104@qq.com>
Co-committed-by: hua <1494133104@qq.com>
This commit is contained in:
hua 2024-07-31 15:26:28 +08:00 committed by sealday
parent bcdf1e7972
commit 54a7f5dc40
29 changed files with 3001 additions and 23 deletions

View File

@ -0,0 +1,2 @@
cache
artifacts

View File

@ -0,0 +1,2 @@
/node_modules
/src

View File

@ -0,0 +1,13 @@
# @tachybase/plugin-blockchain
# 给文件添加执行权限(第一次要输入这些)
chmod +x start_geth.sh
chmod +x compile_contract.sh
chmod +x deploy_contract.sh
# 启动私链
pnpm run geth
# 编译
pnpm run compile
# 部署
pnpm run deploy

View File

@ -0,0 +1,2 @@
export * from './dist/client';
export { default } from './dist/client';

View File

@ -0,0 +1 @@
module.exports = require('./dist/client/index.js');

View File

@ -0,0 +1,3 @@
{
"DataStorage": "0x6176309fF5993b4f93a4De19d2E30bDE5cF0472c"
}

View File

@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract DataStorage {
struct Data {
string hashedData;
string salt;
}
mapping(uint256 => Data) private data;
event DataStored(uint256 indexed id, string hashedData, string salt);
event DataDeleted(uint256 indexed id);
function storeData(uint256 id, string memory hashedData, string memory salt) public {
require(bytes(data[id].hashedData).length == 0, "Data already exists for this ID");
data[id] = Data(hashedData, salt);
emit DataStored(id, hashedData, salt);
}
function getData(uint256 id) public view returns (string memory hashedData, string memory salt) {
Data storage item = data[id];
require(bytes(item.hashedData).length != 0, "Data not found");
return (item.hashedData, item.salt);
}
function deleteData(uint256 id) public {
require(bytes(data[id].hashedData).length != 0, "Data not found");
delete data[id];
emit DataDeleted(id);
}
}

View File

@ -0,0 +1,29 @@
// scripts/deploy.js
const hre = require('hardhat');
const fs = require('fs');
// npx hardhat compile
// npx hardhat run scripts/deploy.js
async function main() {
const [deployer] = await hre.ethers.getSigners();
console.log('Deploying contracts with the account:', deployer.address);
//部署DataStorage合约
const DataStorage = await hre.ethers.getContractFactory('DataStorage');
const dataStorage = await DataStorage.deploy();
console.log('DataStorage deployed to:', dataStorage.target);
// 将DataStorage合约地址保存到一个JSON文件中
const contractAddresses = {
DataStorage: dataStorage.target,
};
fs.writeFileSync('contract-addresses.json', JSON.stringify(contractAddresses, null, 2));
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

View File

@ -0,0 +1,22 @@
require('@nomicfoundation/hardhat-toolbox');
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: {
version: '0.8.20',
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
defaultNetwork: 'gethlocal',
networks: {
gethlocal: {
url: 'http://127.0.0.1:8888/',
chainId: 1337,
},
},
};
//控制台运行 pnpm i @nomicfoundation/hardhat-toolbox

View File

@ -0,0 +1,19 @@
{
"name": "@tachybase/plugin-blockchain",
"version": "0.21.80",
"main": "dist/server/index.js",
"dependencies": {
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"child_process": "^1.0.2",
"crypto": "^1.0.1",
"fs": "0.0.1-security",
"hardhat": "^2.22.6",
"path": "^0.12.7",
"web3": "^4.11.1"
},
"peerDependencies": {
"@tachybase/client": "workspace:*",
"@tachybase/server": "workspace:*",
"@tachybase/test": "workspace:*"
}
}

View File

@ -0,0 +1,2 @@
export * from './dist/server';
export { default } from './dist/server';

View File

@ -0,0 +1 @@
module.exports = require('./dist/server/index.js');

View File

@ -0,0 +1,21 @@
import { Plugin } from '@tachybase/client';
export class PluginBlockchainClient extends Plugin {
async afterAdd() {
// await this.app.pm.add()
}
async beforeLoad() {}
// You can get and modify the app instance here
async load() {
console.log(this.app);
// this.app.addComponents({})
// this.app.addScopes({})
// this.app.addProvider()
// this.app.addProviders()
// this.app.router.add()
}
}
export default PluginBlockchainClient;

View File

@ -0,0 +1,2 @@
export * from './server';
export { default } from './server';

View File

@ -0,0 +1,76 @@
import { Context } from '@tachybase/actions';
import { generateSalt, hashWithSalt } from '../cryptoUtils';
import { contractService } from '../web3/contractService';
// /api/blockchain:store
/**
*
* @param {Context} ctx
*/
export const store = async (ctx: Context) => {
const { data } = ctx.action.params.values;
try {
for (const item of data) {
const salt = generateSalt();
const jsonData = JSON.stringify(item);
const hashedData = hashWithSalt(jsonData, salt);
await contractService.storeData(item.id, hashedData, salt);
}
ctx.body = '数据存储成功';
} catch (error) {
console.error(error);
ctx.status = 500;
ctx.body = '数据存储出错';
}
};
// /api/blockchain:getData?id=xxxx
/**
*
* @param {Context} ctx
*/
export const getDataHandler = async (ctx: Context) => {
const id = ctx.action.params.id;
try {
const { hashedData, salt } = await contractService.getData(id);
ctx.body = { hashedData, salt };
} catch (error) {
console.error(error);
ctx.status = 500;
ctx.body = '检索数据时出错';
}
};
// /api/blockchain:verify
/**
*
* @param {Context} ctx
*/
export const verify = async (ctx: Context) => {
const { data } = ctx.action.params.values;
try {
for (const item of data) {
const { id } = item;
// 获取链上数据
const { hashedData: storedHashedData, salt } = await contractService.getData(id);
const jsonData = JSON.stringify(item);
const hashedData = hashWithSalt(jsonData, salt);
// 比较加密后的结果和链上的加密数据
if (hashedData === storedHashedData) {
console.log(`数据ID: ${id} 验证成功: 数据有效且未被篡改`);
} else {
console.log(`数据ID: ${id} 验证失败: 数据已被篡改或无效`);
}
}
ctx.body = '数据验证完成';
} catch (error) {
console.error(error);
ctx.status = 500;
ctx.body = '验证数据时出错';
}
};
// 事件监听在应用启动时调用
contractService.setupEventListeners();

View File

@ -0,0 +1,25 @@
import { exec } from 'child_process';
import path from 'path';
import { Application } from '@tachybase/server';
export default function (app: Application) {
app
.command('compile')
.option('-v, --version')
.action(async (options) => {
const scriptPath = path.join(__dirname, 'compile_contract.sh');
exec(`bash ${scriptPath}`, (error, stdout, stderr) => {
if (error) {
console.error(`执行脚本时出错: ${error.message}`);
return;
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
console.log(`stdout: ${stdout}`);
});
});
}

View File

@ -0,0 +1,2 @@
cd packages/plugins/@tachybase/plugin-blockchain
npx hardhat compile

View File

@ -0,0 +1,25 @@
import { exec } from 'child_process';
import path from 'path';
import { Application } from '@tachybase/server';
export default function (app: Application) {
app
.command('deploy')
.option('-v, --version')
.action(async (options) => {
const scriptPath = path.join(__dirname, 'deploy_contract.sh');
exec(`bash ${scriptPath}`, (error, stdout, stderr) => {
if (error) {
console.error(`执行脚本时出错: ${error.message}`);
return;
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
console.log(`stdout: ${stdout}`);
});
});
}

View File

@ -0,0 +1,2 @@
cd packages/plugins/@tachybase/plugin-blockchain
npx hardhat run deploy.js

View File

@ -0,0 +1,25 @@
import { exec } from 'child_process';
import path from 'path';
import { Application } from '@tachybase/server';
export default function (app: Application) {
app
.command('geth')
.option('-v, --version')
.action(async (options) => {
const scriptPath = path.join(__dirname, 'start_geth.sh');
exec(`bash ${scriptPath}`, (error, stdout, stderr) => {
if (error) {
console.error(`执行脚本时出错: ${error.message}`);
return;
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
console.log(`stdout: ${stdout}`);
});
});
}

View File

@ -0,0 +1,2 @@
cd ../eth-private-chain/
nohup geth --datadir "." --dev --dev.period 2 --http --http.api eth,web3,net --http.corsdomain "http://remix.ethereum.org" --password password.txt --http.port 8888 --ws --ws.api eth,web3,net --ws.origins "*" --ws.addr "127.0.0.1" --ws.port 8546 > geth_output.log 2>&1 &

View File

@ -0,0 +1,22 @@
import crypto from 'crypto';
/**
*
* @returns {string}
*/
export function generateSalt(): string {
return crypto.randomBytes(16).toString('hex');
}
/**
* 使
* @param {string} data
* @param {string} salt
* @returns {string}
*/
export function hashWithSalt(data: string, salt: string): string {
return crypto
.createHash('sha256')
.update(data + salt)
.digest('hex');
}

View File

@ -0,0 +1 @@
export { default } from './plugin';

View File

@ -0,0 +1,33 @@
import { Plugin } from '@tachybase/server';
import { getDataHandler, store, verify } from './actions/blockchain';
import { contractService } from './web3/contractService';
export class PluginBlockchainServer extends Plugin {
async afterAdd() {}
async beforeLoad() {}
async load() {
this.app.resourcer.define({
name: 'blockchain',
actions: {
store,
getDataHandler,
verify,
},
});
this.app.acl.allow('blockchain', '*', 'public');
}
async install() {}
async afterEnable() {}
async afterDisable() {}
async remove() {}
}
export default PluginBlockchainServer;

View File

@ -0,0 +1,135 @@
import fs from 'fs';
import path from 'path';
import { web3Provider } from './web3Provider';
class ContractService {
private contract: any;
private defaultAccount: string;
private currentContractAbi: any;
private currentContractAddress: string;
constructor() {
this.initializeContract();
setInterval(this.checkForContractAddressOrAbiChange.bind(this), 60000); // 每分钟检查一次
}
/**
*
*/
private initializeContract() {
const web3 = web3Provider.web3;
const account = web3.eth.accounts.privateKeyToAccount(process.env.PRIVATE_KEY as string);
web3.eth.accounts.wallet.add(account);
web3.eth.defaultAccount = account.address;
this.currentContractAbi = this.getContractAbi();
this.currentContractAddress = this.getContractAddress();
this.updateContractInstance(this.currentContractAbi, this.currentContractAddress);
this.defaultAccount = account.address;
}
/**
* ABI
*/
private getContractAbi(): any {
const contractJson = JSON.parse(
fs.readFileSync(
path.resolve(__dirname, '../../../artifacts/contracts/DataStorage.sol/DataStorage.json'),
'utf-8',
),
);
return contractJson.abi;
}
/**
*
*/
private getContractAddress(): string {
const contractAddresses = JSON.parse(
fs.readFileSync(path.resolve(__dirname, '../../../contract-addresses.json'), 'utf-8'),
);
return contractAddresses.DataStorage;
}
/**
* ABI是否发生变化
*/
private checkForContractAddressOrAbiChange() {
console.log('正在检查合约地址或 ABI 是否发生变化...');
const contractABI = this.getContractAbi();
const contractAddress = this.getContractAddress();
if (
contractAddress !== this.currentContractAddress ||
JSON.stringify(contractABI) !== JSON.stringify(this.currentContractAbi)
) {
console.log(`检测到合约地址或 ABI 发生变化。更新合约实例为新地址: ${contractAddress}`);
this.updateContractInstance(contractABI, contractAddress);
} else {
console.log('未检测到合约地址或 ABI 发生变化。');
}
}
/**
*
* @param {any} newAbi ABI
* @param {string} newAddress
*/
private updateContractInstance(newAbi: any, newAddress: string) {
this.currentContractAbi = newAbi;
this.currentContractAddress = newAddress;
this.contract = new web3Provider.web3.eth.Contract(newAbi, newAddress);
}
/**
*
* @param {string} id
* @param {string} hashedData
* @param {string} salt 使
*/
async storeData(id: string, hashedData: string, salt: string): Promise<void> {
this.checkForContractAddressOrAbiChange();
await this.contract.methods.storeData(id, hashedData, salt).send({ from: this.defaultAccount });
}
/**
*
* @param {string} id
* @returns {Promise<{ hashedData: string; salt: string }>}
*/
async getData(id: string): Promise<{ hashedData: string; salt: string }> {
this.checkForContractAddressOrAbiChange();
return this.contract.methods.getData(id).call();
}
// 监听合约事件
setupEventListeners() {
this.checkForContractAddressOrAbiChange();
// 监听 DataStored 事件
const dataStoredSubscription = this.contract.events.DataStored({ fromBlock: 'latest' });
dataStoredSubscription.on('data', (event) => {
console.log('交易hash:' + event.transactionHash);
console.log('区块高度:' + event.blockNumber);
console.log('DataStored event:', event.returnValues);
});
dataStoredSubscription.on('error', (error) => {
console.error('Error on DataStored event:', error);
});
// 监听 DataDeleted 事件
const dataDeletedSubscription = this.contract.events.DataDeleted({ fromBlock: 'latest' });
dataDeletedSubscription.on('data', (event) => {
console.log('交易hash:' + event.transactionHash);
console.log('区块高度:' + event.blockNumber);
console.log('DataDeleted event:', event.returnValues);
});
dataDeletedSubscription.on('error', (error) => {
console.error('Error on DataDeleted event:', error);
});
}
}
export const contractService = new ContractService();

View File

@ -0,0 +1,44 @@
import { Web3 } from 'web3';
export class Web3Provider {
public web3: Web3;
constructor() {
this.web3 = new Web3(new Web3.providers.WebsocketProvider('ws://127.0.0.1:8546'));
this.initializeWebSocket();
}
private initializeWebSocket() {
this.web3.currentProvider.on('connect', () => {
console.log('WebSocket connected');
});
this.web3.currentProvider.on('error', (error) => {
console.error('WebSocket error:', error);
});
this.web3.currentProvider.on('end', () => {
console.log('WebSocket connection closed. Reconnecting...');
this.connectWebSocket();
});
}
private connectWebSocket() {
this.web3.setProvider(new Web3.providers.WebsocketProvider('ws://127.0.0.1:8546'));
this.web3.currentProvider.on('connect', () => {
console.log('WebSocket reconnected');
});
this.web3.currentProvider.on('error', (error) => {
console.error('WebSocket error:', error);
});
this.web3.currentProvider.on('end', () => {
console.log('WebSocket connection closed again. Reconnecting...');
this.connectWebSocket();
});
}
}
export const web3Provider = new Web3Provider();

View File

@ -20,7 +20,7 @@ async function request(config, context) {
const { token, origin } = context;
const { method = 'POST', data, timeout = 5000 } = config;
const originUrl = (config.url?.url || '').trim();
const originUrl = config.url?.trim() || '';
const url = originUrl.startsWith('http') ? originUrl : `${origin}${originUrl}`;
let headers = (config.headers ?? []).reduce((result, header) => {

File diff suppressed because it is too large Load Diff