diff --git a/apps/whale-api/src/app.module.ts b/apps/whale-api/src/app.module.ts index ca429778fb..6bc1162a20 100644 --- a/apps/whale-api/src/app.module.ts +++ b/apps/whale-api/src/app.module.ts @@ -6,6 +6,7 @@ import { ApiModule } from '@src/module.api' import { DatabaseModule } from '@src/module.database/module' import { DeFiDModule } from '@src/module.defid' import { HealthModule } from '@src/module.health' +import { ModelModule } from '@src/module.model/_module' import { AppConfiguration } from '@src/app.configuration' @Module({}) @@ -20,6 +21,7 @@ export class AppModule { }), ScheduleModule.forRoot(), DatabaseModule.forRoot(provider), + ModelModule, DeFiDModule, HealthModule, ApiModule diff --git a/apps/whale-api/src/module.model/_hex.encoder.spec.ts b/apps/whale-api/src/module.model/_hex.encoder.spec.ts new file mode 100644 index 0000000000..4d8c0a7632 --- /dev/null +++ b/apps/whale-api/src/module.model/_hex.encoder.spec.ts @@ -0,0 +1,31 @@ +import { HexEncoder } from '@src/module.model/_hex.encoder' + +it('should encode script hex', () => { + const hex = HexEncoder.asSHA256('1600140e7c0ab18b305bc987a266dc06de26fcfab4b56a') + expect(hex).toBe('3d78c27dffed5c633ec8cb3c1bab3aec7c63ff9247cd2a7646f98e4f7075cca0') + expect(hex.length).toBe(64) +}) + +it('should encode height', () => { + expect(HexEncoder.encodeHeight(0)).toBe('00000000') + expect(HexEncoder.encodeHeight(1)).toBe('00000001') + expect(HexEncoder.encodeHeight(255)).toBe('000000ff') + expect(HexEncoder.encodeHeight(256)).toBe('00000100') + expect(HexEncoder.encodeHeight(4294967295)).toBe('ffffffff') + + expect(() => { + HexEncoder.encodeHeight(4294967296) + }).toThrow('max 32 bits but number larger than 4294967295') +}) + +it('should encode vout index', () => { + expect(HexEncoder.encodeVoutIndex(0)).toBe('00000000') + expect(HexEncoder.encodeVoutIndex(1)).toBe('00000001') + expect(HexEncoder.encodeVoutIndex(255)).toBe('000000ff') + expect(HexEncoder.encodeVoutIndex(256)).toBe('00000100') + expect(HexEncoder.encodeVoutIndex(4294967295)).toBe('ffffffff') + + expect(() => { + HexEncoder.encodeVoutIndex(4294967296) + }).toThrow('max 32 bits but number larger than 4294967295') +}) diff --git a/apps/whale-api/src/module.model/_hex.encoder.ts b/apps/whale-api/src/module.model/_hex.encoder.ts new file mode 100644 index 0000000000..90a9ec704c --- /dev/null +++ b/apps/whale-api/src/module.model/_hex.encoder.ts @@ -0,0 +1,39 @@ +import { SHA256 } from '@defichain/jellyfish-crypto' + +/** + * HexEncoder to encode various DeFi transaction data into fixed length hex + * This allow the use of fixed length sorting in LSM database. + */ +export const HexEncoder = { + /** + * @param {string} hex to encode into HID 32 bytes hex-encoded string + * @return {string} fixed length of 32 byte + */ + asSHA256 (hex: string): string { + const buffer = Buffer.from(hex, 'hex') + return SHA256(buffer).toString('hex') + }, + + /** + * 4 byte hex, Max Number = 4294967295 + * @param {number} height from block to hex, about 4000 years + * @return {string} fixed length of 4 byte + */ + encodeHeight (height: number): string { + if (height > 4294967295) { + throw new Error('max 32 bits but number larger than 4294967295') + } + return height.toString(16).padStart(8, '0') + }, + /** + * 4 byte hex, Max Number = 4294967295 + * @param {number} n from vout to hex, 4 bytes max consensus rule + * @return {string} fixed length of 4 byte + */ + encodeVoutIndex (n: number): string { + if (n > 4294967295) { + throw new Error('max 32 bits but number larger than 4294967295') + } + return n.toString(16).padStart(8, '0') + } +} diff --git a/apps/whale-api/src/module.model/_module.ts b/apps/whale-api/src/module.model/_module.ts new file mode 100644 index 0000000000..d7887b20f1 --- /dev/null +++ b/apps/whale-api/src/module.model/_module.ts @@ -0,0 +1,25 @@ +import { Global, Module } from '@nestjs/common' +import { RawBlockMapper } from '@src/module.model/raw.block' +import { BlockMapper } from '@src/module.model/block' +import { ScriptActivityMapper } from '@src/module.model/script.activity' +import { ScriptAggregationMapper } from '@src/module.model/script.aggregation' +import { ScriptUnspentMapper } from '@src/module.model/script.unspent' +import { TransactionMapper } from '@src/module.model/transaction' +import { TransactionVinMapper } from '@src/module.model/transaction.vin' +import { TransactionVoutMapper } from '@src/module.model/transaction.vout' + +@Global() +@Module({ + providers: [ + RawBlockMapper, + BlockMapper, + ScriptActivityMapper, + ScriptAggregationMapper, + ScriptUnspentMapper, + TransactionMapper, + TransactionVinMapper, + TransactionVoutMapper + ] +}) +export class ModelModule { +} diff --git a/apps/whale-api/src/module.model/block.spec.ts b/apps/whale-api/src/module.model/block.spec.ts new file mode 100644 index 0000000000..5dc1a9bc1a --- /dev/null +++ b/apps/whale-api/src/module.model/block.spec.ts @@ -0,0 +1,114 @@ +import { Database } from '@src/module.database/database' +import { Test } from '@nestjs/testing' +import { MemoryDatabaseModule } from '@src/module.database/provider.memory/module' +import { LevelDatabase } from '@src/module.database/provider.level/level.database' +import { BlockMapper } from '@src/module.model/block' +import assert from 'assert' + +let database: Database +let mapper: BlockMapper + +beforeAll(async () => { + const app = await Test.createTestingModule({ + imports: [MemoryDatabaseModule], + providers: [BlockMapper] + }).compile() + + database = app.get(Database) + mapper = app.get(BlockMapper) +}) + +afterAll(async () => { + await (database as LevelDatabase).close() +}) + +beforeEach(async () => { + async function put (height: number, hash: string): Promise { + await mapper.put({ + difficulty: 0, + id: hash, + hash: hash, + height: height, + masternode: '', + median_time: 0, + merkleroot: '', + minter: '', + minter_block_count: 0, + previous_hash: '', + size: 0, + size_stripped: 0, + stake_modifier: '', + time: 0, + transaction_count: 0, + version: 0, + weight: 0 + }) + } + + await put(0, '0000000000000000000000000000000000000000000000000000000000000000') + await put(1, '1000000000000000000000000000000000000000000000000000000000000000') + await put(2, '1000000000000000000000000000000010000000000000000000000000000000') +}) + +afterEach(async () => { + await (database as LevelDatabase).clear() +}) + +it('should getByHash', async () => { + const block = await mapper.getByHash('1000000000000000000000000000000000000000000000000000000000000000') + expect(block?.height).toBe(1) +}) + +it('should getByHeight', async () => { + const block = await mapper.getByHeight(0) + expect(block?.hash).toBe('0000000000000000000000000000000000000000000000000000000000000000') +}) + +it('should getHighest', async () => { + const block = await mapper.getHighest() + expect(block?.height).toBe(2) + expect(block?.hash).toBe('1000000000000000000000000000000010000000000000000000000000000000') +}) + +it('should queryByHeight', async () => { + const blocks = await mapper.queryByHeight(10) + expect(blocks.length).toBe(3) + + expect(blocks[0].height).toBe(2) + expect(blocks[1].height).toBe(1) + expect(blocks[2].height).toBe(0) + + const after2 = await mapper.queryByHeight(10, 2) + expect(after2[0].height).toBe(1) + expect(after2[1].height).toBe(0) +}) + +it('should put', async () => { + const block = await mapper.getByHeight(0) + assert(block !== undefined) + block.size = 100 + await mapper.put(block) + + const updated = await mapper.getByHeight(0) + expect(updated?.size).toBe(100) +}) + +it('should put but deleted', async () => { + const block = await mapper.getByHeight(0) + assert(block !== undefined) + + block.height = 3 + await mapper.put(block) + + const deleted = await mapper.getByHeight(0) + expect(deleted).toBeUndefined() + + const updated = await mapper.getByHeight(3) + expect(updated).toBeTruthy() +}) + +it('should delete', async () => { + await mapper.delete('0000000000000000000000000000000000000000000000000000000000000000') + const deleted = await mapper.getByHeight(0) + expect(deleted).toBeUndefined() +}) diff --git a/apps/whale-api/src/module.models/block.ts b/apps/whale-api/src/module.model/block.ts similarity index 88% rename from apps/whale-api/src/module.models/block.ts rename to apps/whale-api/src/module.model/block.ts index 86470df169..d89dce10cc 100644 --- a/apps/whale-api/src/module.models/block.ts +++ b/apps/whale-api/src/module.model/block.ts @@ -5,13 +5,6 @@ import { Database, SortOrder } from '@src/module.database/database' const BlockMapping: ModelMapping = { type: 'block', index: { - hash: { - name: 'block_hash', - partition: { - type: 'string', - key: (d: Block) => d.hash - } - }, height: { name: 'block_height', partition: { @@ -23,19 +16,19 @@ const BlockMapping: ModelMapping = { } @Injectable() -export class BlockDbMapper { +export class BlockMapper { public constructor (protected readonly database: Database) { } async getByHash (hash: string): Promise { - return await this.database.get(BlockMapping.index.hash, hash) + return await this.database.get(BlockMapping, hash) } async getByHeight (height: number): Promise { return await this.database.get(BlockMapping.index.height, height) } - async getBest (): Promise { + async getHighest (): Promise { const blocks = await this.database.query(BlockMapping.index.height, { order: SortOrder.DESC, limit: 1 diff --git a/apps/whale-api/src/module.model/raw.block.spec.ts b/apps/whale-api/src/module.model/raw.block.spec.ts new file mode 100644 index 0000000000..0c935049a3 --- /dev/null +++ b/apps/whale-api/src/module.model/raw.block.spec.ts @@ -0,0 +1,51 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { Database } from '@src/module.database/database' +import { Test } from '@nestjs/testing' +import { MemoryDatabaseModule } from '@src/module.database/provider.memory/module' +import { LevelDatabase } from '@src/module.database/provider.level/level.database' +import { RawBlockMapper } from '@src/module.model/raw.block' +import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' + +const container = new MasterNodeRegTestContainer() +let client: JsonRpcClient +let database: Database +let mapper: RawBlockMapper + +beforeAll(async () => { + await container.start() + await container.waitForReady() + await container.generate(3) + client = new JsonRpcClient(await container.getCachedRpcUrl()) + + const app = await Test.createTestingModule({ + imports: [MemoryDatabaseModule], + providers: [RawBlockMapper] + }).compile() + + database = app.get(Database) + mapper = app.get(RawBlockMapper) +}) + +afterAll(async () => { + await (database as LevelDatabase).close() + await container.stop() +}) + +it('should put from defid and get back same object from mapper', async () => { + const hash = await client.blockchain.getBlockHash(1) + const block = await client.blockchain.getBlock(hash, 2) + await mapper.put(block) + + const saved = await mapper.get(hash) + expect(saved).toEqual(block) +}) + +it('should delete and be deleted', async () => { + const hash = await client.blockchain.getBlockHash(2) + const block = await client.blockchain.getBlock(hash, 2) + await mapper.put(block) + await mapper.delete(hash) + + const deleted = await mapper.get(hash) + expect(deleted).toBeUndefined() +}) diff --git a/apps/whale-api/src/module.model/raw.block.ts b/apps/whale-api/src/module.model/raw.block.ts new file mode 100644 index 0000000000..1f5f54576d --- /dev/null +++ b/apps/whale-api/src/module.model/raw.block.ts @@ -0,0 +1,51 @@ +import { Injectable } from '@nestjs/common' +import { Model, ModelMapping } from '@src/module.database/model' +import { Database } from '@src/module.database/database' +import { blockchain as defid } from '@defichain/jellyfish-api-core' +import { JellyfishJSON } from '@defichain/jellyfish-json' + +const RawBlockMapping: ModelMapping = { + type: 'raw_block', + index: {} +} + +/** + * RawBlock data from defid with verbose 2, Block + * Data indexed in store for module.sync to reorg index in event of chain split. + */ +@Injectable() +export class RawBlockMapper { + public constructor (protected readonly database: Database) { + } + + async get (hash: string): Promise | undefined> { + const cached = await this.database.get(RawBlockMapping, hash) + if (cached === undefined) { + return undefined + } + + return JellyfishJSON.parse(cached.json, { + tx: { + vout: { + value: 'bignumber' + } + } + }) + } + + async put (block: defid.Block): Promise { + return await this.database.put(RawBlockMapping, { + id: block.hash, + json: JellyfishJSON.stringify(block) + }) + } + + async delete (hash: string): Promise { + return await this.database.delete(RawBlockMapping, hash) + } +} + +export interface RawBlock extends Model { + id: string // -----------------| hash + json: string // ---------------| block encoded in JSON with JellyfishJSON from defid with verbose 2 +} diff --git a/apps/whale-api/src/module.model/script.activity.spec.ts b/apps/whale-api/src/module.model/script.activity.spec.ts new file mode 100644 index 0000000000..0f24d621c0 --- /dev/null +++ b/apps/whale-api/src/module.model/script.activity.spec.ts @@ -0,0 +1,83 @@ +import { Database } from '@src/module.database/database' +import { Test } from '@nestjs/testing' +import { MemoryDatabaseModule } from '@src/module.database/provider.memory/module' +import { LevelDatabase } from '@src/module.database/provider.level/level.database' +import { ScriptActivityMapper, ScriptActivityType } from '@src/module.model/script.activity' +import { HexEncoder } from '@src/module.model/_hex.encoder' + +let database: Database +let mapper: ScriptActivityMapper + +beforeAll(async () => { + const app = await Test.createTestingModule({ + imports: [MemoryDatabaseModule], + providers: [ScriptActivityMapper] + }).compile() + + database = app.get(Database) + mapper = app.get(ScriptActivityMapper) +}) + +afterAll(async () => { + await (database as LevelDatabase).close() +}) + +beforeEach(async () => { + async function put (hex: string, height: number, type: ScriptActivityType, txid: string, n: number): Promise { + await mapper.put({ + id: HexEncoder.encodeHeight(height) + ScriptActivityMapper.typeAsHex(type) + txid + HexEncoder.encodeVoutIndex(n), + hid: HexEncoder.asSHA256(hex), + block: { hash: '', height: height }, + script: { hex: hex, type: '' }, + txid: txid, + type: type, + type_hex: ScriptActivityMapper.typeAsHex(type), + value: '1.00' + }) + } + + const hex = '1600140e7c0ab18b305bc987a266dc06de26fcfab4b56a' + + function randomTxid (): string { + return (Math.random() * 9999999999999999).toString(16).padStart(64, '0') + } + + await put(hex, 0, 'vin', randomTxid(), 0) + await put(hex, 0, 'vout', randomTxid(), 1) + await put(hex, 1, 'vin', randomTxid(), 0) + await put(hex, 1, 'vout', randomTxid(), 1) +}) + +afterEach(async () => { + await (database as LevelDatabase).clear() +}) + +it('should query', async () => { + const hex = '1600140e7c0ab18b305bc987a266dc06de26fcfab4b56a' + const hid = HexEncoder.asSHA256(hex) + const list = await mapper.query(hid, 10) + + expect(list.length).toBe(4) + + expect(list[0].block.height).toBe(1) + expect(list[0].type_hex).toBe('01') + + expect(list[1].block.height).toBe(1) + expect(list[1].type_hex).toBe('00') + + expect(list[2].block.height).toBe(0) + expect(list[2].type_hex).toBe('01') + + expect(list[3].block.height).toBe(0) + expect(list[3].type_hex).toBe('00') +}) + +it('should delete', async () => { + const hex = '1600140e7c0ab18b305bc987a266dc06de26fcfab4b56a' + const hid = HexEncoder.asSHA256(hex) + const list = await mapper.query(hid, 10) + + await mapper.delete(list[0].id) + const deleted = await mapper.query(hid, 10) + expect(deleted.length).toBe(3) +}) diff --git a/apps/whale-api/src/module.model/script.activity.ts b/apps/whale-api/src/module.model/script.activity.ts new file mode 100644 index 0000000000..375c902491 --- /dev/null +++ b/apps/whale-api/src/module.model/script.activity.ts @@ -0,0 +1,94 @@ +import { Injectable } from '@nestjs/common' +import { Model, ModelMapping } from '@src/module.database/model' +import { Database, SortOrder } from '@src/module.database/database' + +const ScriptActivityMapping: ModelMapping = { + type: 'script_activity', + index: { + hid_id: { + name: 'script_activity_hid_id', + partition: { + type: 'string', + key: (d: ScriptActivity) => d.hid + }, + sort: { + type: 'string', + key: (d: ScriptActivity) => d.id + } + } + } +} + +@Injectable() +export class ScriptActivityMapper { + public constructor (protected readonly database: Database) { + } + + async query (hid: string, limit: number, lt?: string): Promise { + return await this.database.query(ScriptActivityMapping.index.hid_id, { + partitionKey: hid, + limit: limit, + order: SortOrder.DESC, + lt: lt + }) + } + + async put (activity: ScriptActivity): Promise { + return await this.database.put(ScriptActivityMapping, activity) + } + + async delete (id: string): Promise { + return await this.database.delete(ScriptActivityMapping, id) + } + + static typeAsHex (type: ScriptActivityType): ScriptActivityTypeHex { + switch (type) { + case 'vin': + return ScriptActivityTypeHex.VIN + case 'vout': + return ScriptActivityTypeHex.VOUT + } + } +} + +export type ScriptActivityType = 'vin' | 'vout' + +export enum ScriptActivityTypeHex { + VIN = '00', + VOUT = '01', +} + +/** + * Script moving activity + */ +export interface ScriptActivity extends Model { + id: string // ----------------| unique id of this output: block height, type, txid(vin/vout), n(vin/vout) + hid: string // ---------------| hashed id, for length compatibility reasons this is the hashed id of script + + type: ScriptActivityType + type_hex: ScriptActivityTypeHex + txid: string // --------------| txn that created the script activity + + block: { + hash: string + height: number + } + + script: { + type: string + hex: string + } + + vin?: { + txid: string + n: number + } + + vout?: { + txid: string + n: number + } + + value: string // -------------| output value stored as string, string as decimal: 0.0000 + token_id?: number // ---------| token id, unused currently, optional before txn v4 +} diff --git a/apps/whale-api/src/module.model/script.aggregation.ts b/apps/whale-api/src/module.model/script.aggregation.ts new file mode 100644 index 0000000000..88cba02c3e --- /dev/null +++ b/apps/whale-api/src/module.model/script.aggregation.ts @@ -0,0 +1,113 @@ +import { Injectable } from '@nestjs/common' +import { Model, ModelMapping } from '@src/module.database/model' +import { Database, SortOrder } from '@src/module.database/database' + +const ScriptAggregationMapping: ModelMapping = { + type: 'script_aggregation', + index: { + hid_height: { + name: 'script_aggregation_hid_height', + partition: { + type: 'string', + key: (d: ScriptAggregation) => d.hid + }, + sort: { + type: 'number', + key: (d: ScriptAggregation) => d.block.height + } + } + } +} + +@Injectable() +export class ScriptAggregationMapper { + public constructor (protected readonly database: Database) { + } + + async getLatest (hid: string): Promise { + const aggregations = await this.database.query(ScriptAggregationMapping.index.hid_height, { + partitionKey: hid, + order: SortOrder.DESC, + limit: 1 + }) + return aggregations.length === 0 ? undefined : aggregations[0] + } + + async query (hid: string, limit: number, lt?: number): Promise { + return await this.database.query(ScriptAggregationMapping.index.hid_height, { + partitionKey: hid, + limit: limit, + order: SortOrder.DESC, + lt: lt + }) + } + + async put (aggregation: ScriptAggregation): Promise { + return await this.database.put(ScriptAggregationMapping, aggregation) + } + + async delete (id: string): Promise { + return await this.database.delete(ScriptAggregationMapping, id) + } +} + +/** + * ScriptAggregation represent a directed acyclic graph node of script activity in the blockchain. + * ScriptAggregation is a vertex that forms a linear forward moving aggregation of script activity. + * + * ScriptAggregation uses a composite key, the partition key is the script hex that is hashed + * for length compatibility reasons as the max of 10kb is too large for many database provider. + * The sort key is the height of the block that generated this aggregation. If there is no script + * activity at a particular block height, ScriptAggregation will not be created at that block height. + * + * To query the latest script activity aggregation in the blockchain, you can query hex_height + * index with (sort by desc and limit 1). That will always return the latest script activity in + * the chain in the database. + * + * Blocks | Block 1 | Block 2 | Block 3 | Block 4 | Block 5 | Block 6 | + * --------|---------|---------|---------|---------|---------|---------| + * Txn | out | in | | | | | + * Inputs | | out | | | in | | + * & | | | out | | in | | + * Outputs | | | | | out | in | + * --------|---------|---------|---------|---------|---------|---------| + * Hash | 0 | prev | prev | | prev | prev | + * | + out | + in | + out | | + in | + in | + * | | + out | | | + in | | + * | | | | | + out | | + * --------|---------|---------|---------|---------|---------|---------| + * Agg | + $ | - $ | + $ | | - $ | - $ | + * Amount | | + $ | | | - $ | | + * | | | | | + $ | | + * --------|---------|---------|---------|---------|---------|---------| + * Agg | out +=1 | out +=1 | out +=1 | | out +=1 | | = 4 + * Count | | in +=1 | | | in +=2 | in +=1 | = 4 + * | sum +=1 | sum +=2 | sum +=1 | | sum +=3 | sum +=1 | = 8 + * --------|---------|---------|---------|---------|---------|---------| + */ +export interface ScriptAggregation extends Model { + id: string // ------------------| unique id of this output: block height + hid + hid: string // -----------------| hashed id, for length compatibility reasons this is the hashed id of script + + block: { + hash: string // --------------| block hash of this script aggregation + height: number // ------------| block height of this script aggregation + } + + script: { + type: string // --------------| known type of the script + hex: string // ---------------| script in encoded in hex + } + + statistic: { + tx_count: number // ----------| total num of in & out transaction up to block height, see above + tx_in_count: number // -------| total num of transaction going in up to block height, see above + tx_out_count: number // ------| total num of transaction going out up to block height, see above + } + + amount: { // -------------------| stored as string, string as decimal: 0.0000 + tx_in: string // -------------| sum of all value going in up to block height + tx_out: string // ------------| sum of all value going out up to block height + unspent: string // -----------| sum of all unspent value up to block height + } +} diff --git a/apps/whale-api/src/module.model/script.unspent.ts b/apps/whale-api/src/module.model/script.unspent.ts new file mode 100644 index 0000000000..553ffc356e --- /dev/null +++ b/apps/whale-api/src/module.model/script.unspent.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@nestjs/common' +import { Database, SortOrder } from '@src/module.database/database' +import { Model, ModelMapping } from '@src/module.database/model' + +const ScriptUnspentMapping: ModelMapping = { + type: 'script_unspent', + index: { + hid_id: { + name: 'script_unspent_hid_id', + partition: { + type: 'string', + key: (d: ScriptUnspent) => d.hid + }, + sort: { + type: 'string', + key: (d: ScriptUnspent) => d.id + } + } + } +} + +@Injectable() +export class ScriptUnspentMapper { + public constructor (protected readonly database: Database) { + } + + async query (hid: string, limit: number, gt?: string): Promise { + return await this.database.query(ScriptUnspentMapping.index.hid_id, { + partitionKey: hid, + limit: limit, + order: SortOrder.ASC, + gt: gt + }) + } + + async put (aggregation: ScriptUnspent): Promise { + return await this.database.put(ScriptUnspentMapping, aggregation) + } + + async delete (id: string): Promise { + return await this.database.delete(ScriptUnspentMapping, id) + } +} + +export interface ScriptUnspent extends Model { + id: string // ----------------| unique id of this output: vout.txid and vout.n + hid: string // ---------------| hashed id, for length compatibility reasons this is the hashed id of script + sort: string // --------------| sort key: block height, vout.txid and vout.n + + block: { + hash: string // ------------| block hash of this script unspent + height: number // ----------| block height of this script unspent + } + + script: { + type: string // ------------| known type of the script + hex: string // -------------| script in encoded in hex + } + + vout: { + txid: string // ------------| txn that created this unspent + n: number // ---------------| index number of this output within the transaction, if coinbase it will be 0 + value: string // -----------| output value stored as string, string as decimal: 0.0000 + dct_id?: number // ---------| dct id, unused currently, optional before txn v4 + } +} diff --git a/apps/whale-api/src/module.model/transaction.ts b/apps/whale-api/src/module.model/transaction.ts new file mode 100644 index 0000000000..15bed5bae4 --- /dev/null +++ b/apps/whale-api/src/module.model/transaction.ts @@ -0,0 +1,72 @@ +import { Injectable } from '@nestjs/common' +import { Model, ModelMapping } from '@src/module.database/model' +import { Database, SortOrder } from '@src/module.database/database' + +const TransactionMapping: ModelMapping = { + type: 'transaction', + index: { + block_txid: { + name: 'transaction_block_txid', + partition: { + type: 'string', + key: (b: Transaction) => b.block.hash + }, + sort: { + type: 'string', + key: (b: Transaction) => b.txid + } + } + } +} + +@Injectable() +export class TransactionMapper { + public constructor (protected readonly database: Database) { + } + + async queryByBlockHash (hash: string, limit: number, gt?: string): Promise { + return await this.database.query(TransactionMapping.index.block_txid, { + partitionKey: hash, + limit: limit, + order: SortOrder.ASC, + gt: gt + }) + } + + async get (txid: string): Promise { + return await this.database.get(TransactionMapping, txid) + } + + async put (txn: Transaction): Promise { + return await this.database.put(TransactionMapping, txn) + } + + async delete (txid: string): Promise { + return await this.database.delete(TransactionMapping, txid) + } +} + +/** + * Transaction that are included in a block. + */ +export interface Transaction extends Model { + id: string // ----------------| unique id of the transaction, same as the txid + + block: { + hash: string + height: number + } + + txid: string + hash: string + version: number + + size: number + v_size: number + weight: number + + lock_time: number + + vin_count: number + vout_count: number +} diff --git a/apps/whale-api/src/module.model/transaction.vin.ts b/apps/whale-api/src/module.model/transaction.vin.ts new file mode 100644 index 0000000000..20271ca4af --- /dev/null +++ b/apps/whale-api/src/module.model/transaction.vin.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@nestjs/common' +import { Model, ModelMapping } from '@src/module.database/model' +import { Database, SortOrder } from '@src/module.database/database' + +const TransactionVinMapping: ModelMapping = { + type: 'transaction_vin', + index: { + txid_id: { + name: 'transaction_vin_txid_id', + partition: { + type: 'string', + key: (d: TransactionVin) => d.txid + }, + sort: { + type: 'string', + key: (d: TransactionVin) => d.id + } + } + } +} + +@Injectable() +export class TransactionVinMapper { + public constructor (protected readonly database: Database) { + } + + /** + * @param {string} txid of partition + * @param {number} limit number of results + * @param {string} gt vout.id + */ + async query (txid: string, limit: number, gt?: string): Promise { + return await this.database.query(TransactionVinMapping.index.txid_id, { + partitionKey: txid, + limit: limit, + order: SortOrder.ASC, + gt: gt + }) + } + + async put (vin: TransactionVin): Promise { + return await this.database.put(TransactionVinMapping, vin) + } + + async delete (id: string): Promise { + return await this.database.delete(TransactionVinMapping, id) + } +} + +export interface TransactionVin extends Model { + id: string // ----------------| unique id of the vin: txid + vout.txid + (vout.n 4 bytes encoded hex) + // ---------------------------| if coinbase transaction: txid + '00' + + txid: string // --------------| transaction id that this vin belongs to + coinbase: string + + vout?: { // ------------------| id, txid, n and the exact same as TransactionVout + id: string + txid: string + n: number + value: string + dct_id?: number + } + + script: { + hex: string + } + + tx_in_witness: string[] + sequence: string +} diff --git a/apps/whale-api/src/module.model/transaction.vout.ts b/apps/whale-api/src/module.model/transaction.vout.ts new file mode 100644 index 0000000000..6a73fad317 --- /dev/null +++ b/apps/whale-api/src/module.model/transaction.vout.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@nestjs/common' +import { Model, ModelMapping } from '@src/module.database/model' +import { Database, SortOrder } from '@src/module.database/database' +import { HexEncoder } from '@src/module.model/_hex.encoder' + +const TransactionVoutMapping: ModelMapping = { + type: 'transaction_vout', + index: { + txid_n: { + name: 'transaction_vout_txid_n', + partition: { + type: 'string', + key: (d: TransactionVout) => d.txid + }, + sort: { + type: 'number', + key: (d: TransactionVout) => d.n + } + } + } +} + +@Injectable() +export class TransactionVoutMapper { + public constructor (protected readonly database: Database) { + } + + /** + * @param {string} txid of partition + * @param {number} limit number of results + * @param {string} gt n + */ + async query (txid: string, limit: number, gt?: number): Promise { + return await this.database.query(TransactionVoutMapping.index.txid_n, { + partitionKey: txid, + limit: limit, + order: SortOrder.ASC, + gt: gt + }) + } + + async get (txid: string, n: number): Promise { + return await this.database.get(TransactionVoutMapping, txid + HexEncoder.encodeVoutIndex(n)) + } + + async put (vout: TransactionVout): Promise { + return await this.database.put(TransactionVoutMapping, vout) + } + + async delete (id: string): Promise { + return await this.database.delete(TransactionVoutMapping, id) + } +} + +export interface TransactionVout extends Model { + id: string // ----------------| unique id of the vout: (txid + (n 4 bytes encoded hex)) + txid: string // --------------| transaction id that this vout belongs to + n: number // -----------------| index of the output in the transaction + + value: string // -------------| output value stored as string, string as decimal: 0.0000 + token_id: number // ----------| currently disabled, will always be 0 + + script: { + hex: string + type: string + } +}