Skip to content

Commit

Permalink
Merge pull request #5 from taraldefi/feat/property-selection
Browse files Browse the repository at this point in the history
feat(marketplace): property selection
  • Loading branch information
dorucioclea authored Jul 29, 2023
2 parents 9fae370 + 5c01359 commit af896b8
Show file tree
Hide file tree
Showing 11 changed files with 220 additions and 43 deletions.
53 changes: 52 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,55 @@ Separation of contract logic and data is extremely important in the case of vers

* Run normal tests with `clarinet test`

* Run chainhook tests with `clarinet test --chainhooks ./chainhooks/marketplace.chainhook.yaml ./tests_chainhook/integration_test.ts`
* Run chainhook tests with `clarinet test --chainhooks ./chainhooks/marketplace.chainhook.yaml ./tests_chainhook/integration_test.ts`

## How to run the POC

* Run

```
> cd infrastructure && docker-compose up
```

This will create the infrastructure (RabbitMq, Prometheus, Grafana, Postgresql)

* Start up the consumer

```
> cd chainhook-consumer && yarn start:dev
```

Make sure to install dependencies first of course

* Start up the subscriber

```
> cd chainhook-subscriber && yarn start:dev
```

Make sure that you installed the dependencies first, and that you've run the migrations

```
> yarn migration:run
```

You can also drop the migrations:

```
> yarn schema:drop
```

* Run the integration test and then have a look at `http://localhost:3003/api/v1/auctionhistory/0`

You should be getting the history of the auction entity you just inserted

```
[
"Auction with the id \"0\" was created at 7/28/2023, 3:46:30 PM. The id was changed to \"10b289cf-1684-4544-b1e0-f2c3b4548b87\". The createdAt was changed to \"2023-07-28T12:46:30.742Z\". The updatedAt was changed to \"2023-07-28T09:46:30.706Z\". The hash was changed to \"5b13b317d797e65992c2064d058cc8ace0dbe490b3026b8a1a76073f02342aa1\". The auctionId was changed to \"0\". The endBlock was changed to \"1000\". The highestBid was changed to \"0\". The maker was changed to \"ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5\". The nftAsset was changed to \"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sip009-nft\". The highestBidder was changed to \"null\". The status was changed to \"OPEN\".",
"Auction bid with the amount \"1200.00\" and bidder \"ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG\" was created at 7/28/2023, 3:46:30 PM. The id was changed to \"997d1955-349e-4a00-bfb8-6d722d0b590d\". The createdAt was changed to \"2023-07-28T12:46:30.935Z\". The updatedAt was changed to \"2023-07-28T09:46:30.883Z\". The hash was changed to \"2ea36fd6353a3b87da323c95fa71ae881c830f7ac6529600242817f9894f9603\". The amount was changed to \"1200.00\". The bidder was changed to \"ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG\".",
"Auction with the id \"0\" was updated at 7/28/2023, 3:46:31 PM. The updatedAt was changed to \"2023-07-28T12:46:30.961Z\". The hash was changed to \"95f95053620e826235584568dfd452ea5af14a9fd5aec0cb6d26b697d27c2e4d\". The highestBid was changed to \"1200.00\". The highestBidder was changed to \"ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG\".",
"Auction bid with the amount \"5000.00\" and bidder \"ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC\" was created at 7/28/2023, 3:46:31 PM. The id was changed to \"01ebce17-9dc9-4dbc-91e6-a4ad4d299711\". The createdAt was changed to \"2023-07-28T12:46:31.056Z\". The updatedAt was changed to \"2023-07-28T09:46:31.026Z\". The hash was changed to \"6a70cc66d818027a29dd5f5af4a12c225be09b6bb7d36dcb610e84194c938500\". The amount was changed to \"5000.00\". The bidder was changed to \"ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC\".",
"Auction with the id \"0\" was updated at 7/28/2023, 3:46:31 PM. The updatedAt was changed to \"2023-07-28T12:46:31.076Z\". The hash was changed to \"2f25544ed3b1e3d32a593e8193be8526f87f0923ee748a535f605771f822b963\". The highestBid was changed to \"5000.00\". The highestBidder was changed to \"ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC\".",
"Auction with the id \"0\" was updated at 7/28/2023, 3:46:31 PM. The updatedAt was changed to \"2023-07-28T12:46:31.225Z\". The hash was changed to \"1bc7cc24cc3bd580a457e26a8cb12a1738b906f1b9f1e43c99f18d6f76931006\". The status was changed to \"CANCELLED\"."
]
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import "reflect-metadata";

export const METADATA_KEY = Symbol('TrackChanges');

export function TrackChanges(): PropertyDecorator {
return function (target: Object, propertyKey: string | symbol): void {
let existingProperties: Array<string | symbol> = Reflect.getMetadata(METADATA_KEY, target.constructor);
if (existingProperties) {
existingProperties.push(propertyKey);
} else {
existingProperties = [propertyKey];
Reflect.defineMetadata(METADATA_KEY, existingProperties, target.constructor);
}
};
}
8 changes: 8 additions & 0 deletions chainhook-subscriber/src/common/entity/custom-base.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,28 @@ import {
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { TrackChanges } from '../decorators/track-changes.decorator';

/**
* custom base entity
*/
export abstract class CustomBaseEntity extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
@TrackChanges()
id: string;

@Column({ type: 'timestamp', precision: 3, default: () => 'CURRENT_TIMESTAMP(3)' })
@TrackChanges()
createdAt: Date;

@Column({ type: 'timestamp', precision: 3, default: () => 'CURRENT_TIMESTAMP(3)' })
@TrackChanges()
updatedAt: Date;

@Column({ type: 'char', length: 64 })
@TrackChanges()
hash: string;

@BeforeInsert()
createTimestamp() {
this.createdAt = new Date();
Expand Down
20 changes: 20 additions & 0 deletions chainhook-subscriber/src/database/migrations/1690548214447-Hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {MigrationInterface, QueryRunner} from "typeorm";

export class Hash1690548214447 implements MigrationInterface {
name = 'Hash1690548214447'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "Auctions" ADD "hash" character(64) NOT NULL`);
await queryRunner.query(`ALTER TABLE "Bids" ADD "hash" character(64) NOT NULL`);
await queryRunner.query(`ALTER TABLE "auction_bids_history" ADD "hash" character(64) NOT NULL`);
await queryRunner.query(`ALTER TABLE "auctions_history" ADD "hash" character(64) NOT NULL`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "auctions_history" DROP COLUMN "hash"`);
await queryRunner.query(`ALTER TABLE "auction_bids_history" DROP COLUMN "hash"`);
await queryRunner.query(`ALTER TABLE "Bids" DROP COLUMN "hash"`);
await queryRunner.query(`ALTER TABLE "Auctions" DROP COLUMN "hash"`);
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { BaseSimpleRepository } from "src/common/repository/base.simple.repository";
import { createHash } from "crypto";
import { METADATA_KEY } from "src/common/decorators/track-changes.decorator";
import { runOnTransactionCommit, runOnTransactionComplete, runOnTransactionRollback } from "src/common/transaction/hook";
import { BaseHistory } from "src/modules/history/entities/base.history.entity";

import * as winston from 'winston';

export abstract class BaseService<T extends { id: string }, H extends BaseHistory> {
export abstract class BaseService {
protected readonly Logger = winston.createLogger({
level: 'info',
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
Expand All @@ -15,30 +16,29 @@ export abstract class BaseService<T extends { id: string }, H extends BaseHistor
});

protected constructor(
protected entity: new () => T,
protected history: new () => H,
protected readonly HRepository: BaseSimpleRepository<H>,

) {
}

protected setupTransactionHooks() {
runOnTransactionRollback((cb) =>
this.Logger.info(`[ROLLBACK] Error: ${cb.message}`),
this.Logger.error(`[ROLLBACK] Error: ${cb.message}`),
);


runOnTransactionComplete((_) => this.Logger.info('[COMMIT] Transaction Complete'));
runOnTransactionCommit(() => this.Logger.info('[COMMIT] Transaction Commit'));
}

protected async insertIntoHistory(oldEntity: T, newEntity: T, action: H['action'],
protected async insertIntoHistory<T extends { id: string }, H extends BaseHistory>(
historyFunc: new () => H,
oldEntity: T, newEntity: T, action: H['action'],
copyEntityToHistory: (entity: T, history: H) => void,
save: (history: H) => Promise<H>) {
this.Logger.info('Inside insertIntoHistory');

const entity = newEntity;

const history = new this.history();
const history = new historyFunc();
history.action = action;
history.createdAt = new Date();

Expand All @@ -51,38 +51,65 @@ export abstract class BaseService<T extends { id: string }, H extends BaseHistor
copyEntityToHistory(entity, history);

this.Logger.info('Inside insertIntoHistory - awaiting save');
console.log(this.stringify(history));

const cache = new Set();
const replacer = (key, value) => {
if (typeof value === 'object' && value !== null) {
if (cache.has(value)) {
// Duplicate reference found, discard key
return;
await save(history);
}

protected calculateHash(entity: any): string {
const properties: Array<string | symbol> = Reflect.getMetadata(METADATA_KEY, entity.constructor) || [];

const obj: { [key: string]: any } = {};

for (const key of properties) {
if (entity.hasOwnProperty(key)) {
obj[String(key)] = entity[key];
}
// Store value in our set
cache.add(value);
}
return value;
};

console.log(JSON.stringify(history, replacer, 2));

await save(history);

const str = this.stringify(obj);
const hash = createHash('sha256');
hash.update(str);
return hash.digest('hex');
}

buildChanges(oldEntity: T, newEntity: T): Array<{ name: string, old_value: any, new_value: any }> {
private buildChanges<T extends { id: string }>(oldEntity: T, newEntity: T): Array<{ name: string, old_value: any, new_value: any }> {
const changes = [];

for (const key in newEntity) {
if (!oldEntity || newEntity[key] !== oldEntity[key]) {
changes.push({
name: key,
old_value: oldEntity ? oldEntity[key] : null,
new_value: newEntity[key],
});
}
const decoratedProperties: Array<string | symbol> = Reflect.getMetadata(METADATA_KEY, newEntity.constructor);

for (const key of decoratedProperties) {
// Skip if the property is not a direct property of newEntity
if (!newEntity.hasOwnProperty(key)) {
continue;
}

if (!oldEntity || newEntity[key] !== oldEntity[key]) {
changes.push({
name: key,
old_value: oldEntity ? oldEntity[key] : null,
new_value: newEntity[key],
});
}
}

return changes;
}

private stringify(obj: any): string {
const cache = new Set();
const replacer = (key, value) => {
if (typeof value === 'object' && value !== null) {
if (cache.has(value)) {
// Duplicate reference found, discard key
return;
}
// Store value in our set
cache.add(value);
}
return value;
};

return JSON.stringify(obj, replacer, 2);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import { AuctionHistoryEntityRepositoryToken } from "../providers/auction.histor
import { AuctionHistoryEntityRepository } from "../repositories/auction.history.repository";

@Injectable()
export class CancelAuctionService extends BaseService<AuctionEntity, AuctionHistoryEntity> {
export class CancelAuctionService extends BaseService {
constructor(
@Inject(AuctionEntityRepositoryToken)
private auctionRepository: AuctionEntityRepository,
@Inject(AuctionHistoryEntityRepositoryToken)
private auctionHistoryRepository: AuctionHistoryEntityRepository,
) {
super(AuctionEntity, AuctionHistoryEntity, auctionHistoryRepository);
super();
}

@Transactional({
Expand All @@ -37,15 +37,21 @@ export class CancelAuctionService extends BaseService<AuctionEntity, AuctionHist
relations: ['bids'],
});

if (auction != null && auction.status == AuctionStatus.CANCELLED) {
this.Logger.info('Auction already cancelled');
return;
}

const oldAuction = Object.assign(Object.create(Object.getPrototypeOf(auction)), auction) as AuctionEntity;

auction.status = AuctionStatus.CANCELLED;
auction.hash = this.calculateHash(auction);

await this.auctionRepository.save(auction);

this.Logger.info('Auction Saved');

await this.insertIntoHistory(oldAuction, auction, "update", (entity: AuctionEntity, history: AuctionHistoryEntity) => {
await this.insertIntoHistory(AuctionHistoryEntity, oldAuction, auction, "update", (entity: AuctionEntity, history: AuctionHistoryEntity) => {
history.auctionId = entity.auctionId;
history.endBlock = entity.endBlock;
history.createdAt = new Date();
Expand All @@ -56,6 +62,7 @@ export class CancelAuctionService extends BaseService<AuctionEntity, AuctionHist
history.highestBidder = entity.highestBidder;
history.nftAsset = entity.nftAsset;
history.status = entity.status;
history.hash = entity.hash;
}, (entity: AuctionHistoryEntity) => this.auctionHistoryRepository.save(entity));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ import { BaseService } from "./base.service";
import { AuctionBidHistoryEntityRepositoryToken } from "../providers/auction.bid.history.repository.provider";
import { AuctionBidHistoryEntityRepository } from "../repositories/auction.bid.history.repository";
import { AuctionBidHistoryEntity } from "../entities/auction.bid.history.entity";
import { AuctionEntity } from "src/modules/auctions/entities/auction.entity";
import { AuctionHistoryEntity } from "../entities/auction.history.entity";
import { AuctionHistoryEntityRepositoryToken } from "../providers/auction.history.repository.provider";
import { AuctionHistoryEntityRepository } from "../repositories/auction.history.repository";

@Injectable()
export class PlaceBidService extends BaseService<AuctionBidEntity, AuctionBidHistoryEntity> {
export class PlaceBidService extends BaseService {

constructor(
@Inject(AuctionEntityRepositoryToken)
Expand All @@ -22,8 +26,10 @@ export class PlaceBidService extends BaseService<AuctionBidEntity, AuctionBidHis
private auctionBidRepository: AuctionBidEntityRepository,
@Inject(AuctionBidHistoryEntityRepositoryToken)
private auctionBidHistoryRepository: AuctionBidHistoryEntityRepository,
@Inject(AuctionHistoryEntityRepositoryToken)
private auctionHistoryRepository: AuctionHistoryEntityRepository,
) {
super(AuctionBidEntity, AuctionBidHistoryEntity, auctionBidHistoryRepository);
super();
}

@Transactional({
Expand All @@ -40,29 +46,54 @@ export class PlaceBidService extends BaseService<AuctionBidEntity, AuctionBidHis
relations: ['bids'],
});

const oldAuction = Object.assign(Object.create(Object.getPrototypeOf(auction)), auction) as AuctionEntity;

const bid = new AuctionBidEntity();
bid.amount = Number(placeBidModel.bid.value);
bid.bidder = placeBidModel.bidder.value;
bid.hash = this.calculateHash(auction);

if (auction.bids.find(b => b.bidder === bid.bidder && b.amount == bid.amount)) {
// same bid, do nothing
this.Logger.info('Same bid, do nothing');
return;
}

await this.auctionBidRepository.save(bid);

(auction.bids || []).push(bid);

auction.highestBidder = bid.bidder;
auction.highestBid = String(bid.amount);
auction.hash = this.calculateHash(auction);

await this.auctionRepository.save(auction);

this.Logger.info('Bid Saved');

this.Logger.info('Inserting into history');

await this.insertIntoHistory(null, bid, "insert", (entity: AuctionBidEntity, history: AuctionBidHistoryEntity) => {
await this.insertIntoHistory(AuctionBidHistoryEntity, null, bid, "insert", (entity: AuctionBidEntity, history: AuctionBidHistoryEntity) => {
history.auctionId = auction.auctionId;
history.createdAt = new Date();
history.bidder = entity.bidder;
history.amount = entity.amount;
}, (entity: AuctionBidHistoryEntity) => this.HRepository.save(entity));
history.hash = entity.hash;
}, (entity: AuctionBidHistoryEntity) => this.auctionBidHistoryRepository.save(entity));

await this.insertIntoHistory(AuctionHistoryEntity, oldAuction, auction, "update", (entity: AuctionEntity, history: AuctionHistoryEntity) => {
history.auctionId = entity.auctionId;
history.endBlock = entity.endBlock;
history.createdAt = new Date();

history.highestBid = entity.highestBid;
history.maker = entity.maker;

history.highestBidder = entity.highestBidder;
history.nftAsset = entity.nftAsset;
history.status = entity.status;
history.hash = entity.hash;
}, (entity: AuctionHistoryEntity) => this.auctionHistoryRepository.save(entity));


this.Logger.info('Inserted into history');
Expand Down
Loading

0 comments on commit af896b8

Please sign in to comment.