Skip to content

Commit

Permalink
Merge pull request #345 from hirosystems/beta
Browse files Browse the repository at this point in the history
release 3.1.0
  • Loading branch information
rafaelcr authored Apr 23, 2024
2 parents 2877071 + 148b6a1 commit 27becac
Show file tree
Hide file tree
Showing 13 changed files with 1,103 additions and 461 deletions.
49 changes: 49 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,52 @@
## [3.1.0-beta.1](https://github.com/hirosystems/ordinals-api/compare/v3.0.2-beta.6...v3.1.0-beta.1) (2024-03-27)


### Features

* support self minted 5-byte wide BRC-20 tokens ([#338](https://github.com/hirosystems/ordinals-api/issues/338)) ([60f46d3](https://github.com/hirosystems/ordinals-api/commit/60f46d3533e837843e8aa6094b4673a3bc84c124))

## [3.0.2-beta.6](https://github.com/hirosystems/ordinals-api/compare/v3.0.2-beta.5...v3.0.2-beta.6) (2024-03-09)


### Bug Fixes

* remove gap detection ([#330](https://github.com/hirosystems/ordinals-api/issues/330)) ([040ee04](https://github.com/hirosystems/ordinals-api/commit/040ee04b0906106fdece3f00d34fb4817d7f318e))

## [3.0.2-beta.5](https://github.com/hirosystems/ordinals-api/compare/v3.0.2-beta.4...v3.0.2-beta.5) (2024-03-09)


### Bug Fixes

* ignore spent as fee on gap check ([#328](https://github.com/hirosystems/ordinals-api/issues/328)) ([a1277cf](https://github.com/hirosystems/ordinals-api/commit/a1277cf39eb61e548f55bd8e524054db6a11c843))

## [3.0.2-beta.4](https://github.com/hirosystems/ordinals-api/compare/v3.0.2-beta.3...v3.0.2-beta.4) (2024-03-08)


### Bug Fixes

* calculate transfer type correctly upon reveal ([#326](https://github.com/hirosystems/ordinals-api/issues/326)) ([6c4c54b](https://github.com/hirosystems/ordinals-api/commit/6c4c54b45a74744c4e61a2437632f390080a9624))

## [3.0.2-beta.3](https://github.com/hirosystems/ordinals-api/compare/v3.0.2-beta.2...v3.0.2-beta.3) (2024-03-07)


### Bug Fixes

* check only the first blessed inscription in next block on gap detection ([#325](https://github.com/hirosystems/ordinals-api/issues/325)) ([9cad6c1](https://github.com/hirosystems/ordinals-api/commit/9cad6c16d34fdd11c1d9f473b2f3802a8da464d8))

## [3.0.2-beta.2](https://github.com/hirosystems/ordinals-api/compare/v3.0.2-beta.1...v3.0.2-beta.2) (2024-03-07)


### Bug Fixes

* filter correct content types for brc20 ([#323](https://github.com/hirosystems/ordinals-api/issues/323)) ([4d52b48](https://github.com/hirosystems/ordinals-api/commit/4d52b4820e5f1a36264977dc4a6c4ca324864108))

## [3.0.2-beta.1](https://github.com/hirosystems/ordinals-api/compare/v3.0.1...v3.0.2-beta.1) (2024-03-07)


### Bug Fixes

* use brc20_total_balances table when inserting new transfer ([#321](https://github.com/hirosystems/ordinals-api/issues/321)) ([925fb0e](https://github.com/hirosystems/ordinals-api/commit/925fb0e05a982eeec802bee6f53a957bc5ea3acf))

## [3.0.1](https://github.com/hirosystems/ordinals-api/compare/v3.0.0...v3.0.1) (2024-03-04)


Expand Down
19 changes: 19 additions & 0 deletions migrations/1711465842961_brc20-deploy-self-mint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate';

export const shorthands: ColumnDefinitions | undefined = undefined;

export function up(pgm: MigrationBuilder): void {
pgm.addColumn('brc20_deploys', {
self_mint: {
type: 'boolean',
default: 'false',
},
});
pgm.sql(`UPDATE brc20_deploys SET self_mint = false`);
pgm.alterColumn('brc20_deploys', 'self_mint', { notNull: true });
}

export function down(pgm: MigrationBuilder): void {
pgm.dropColumn('brc20_deploys', ['self_mint']);
}
1 change: 1 addition & 0 deletions src/api/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ export const Brc20TokenResponseSchema = Type.Object(
deploy_timestamp: Type.Integer({ examples: [1677733170000] }),
minted_supply: Type.String({ examples: ['1000000'] }),
tx_count: Type.Integer({ examples: [300000] }),
self_mint: Type.Boolean(),
},
{ title: 'BRC-20 Token Response' }
);
Expand Down
1 change: 1 addition & 0 deletions src/api/util/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export function parseBrc20Tokens(items: DbBrc20Token[]): Brc20TokenResponse[] {
deploy_timestamp: i.timestamp.valueOf(),
minted_supply: decimals(i.minted_supply, i.decimals),
tx_count: parseInt(i.tx_count),
self_mint: i.self_mint,
}));
}

Expand Down
2 changes: 0 additions & 2 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,6 @@ const schema = Type.Object({

/** Enables BRC-20 processing in write mode APIs */
BRC20_BLOCK_SCAN_ENABLED: Type.Boolean({ default: true }),
/** Enables inscription gap detection to prevent ingesting unordered blocks */
INSCRIPTION_GAP_DETECTION_ENABLED: Type.Boolean({ default: true }),
});
type Env = Static<typeof schema>;

Expand Down
51 changes: 21 additions & 30 deletions src/pg/brc20/brc20-pg-store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { BasePgStoreModule, logger } from '@hirosystems/api-toolkit';
import * as postgres from 'postgres';
import { hexToBuffer } from '../../api/util/helpers';
import {
DbInscriptionIndexPaging,
InscriptionData,
Expand All @@ -27,7 +26,7 @@ import {
DbBrc20TokenWithSupply,
DbBrc20TransferEvent,
} from './types';
import { Brc20Deploy, Brc20Mint, Brc20Transfer, brc20FromInscriptionContent } from './helpers';
import { Brc20Deploy, Brc20Mint, Brc20Transfer, UINT64_MAX, brc20FromInscription } from './helpers';
import { Brc20TokenOrderBy } from '../../api/schemas';
import { objRemoveUndefinedValues } from '../helpers';

Expand All @@ -47,15 +46,7 @@ export class Brc20PgStore extends BasePgStoreModule {
const pointer = args.pointers[i];
if (parseInt(pointer.block_height) < BRC20_GENESIS_BLOCK) continue;
if ('inscription' in reveal) {
if (
reveal.inscription.classic_number < 0 ||
reveal.inscription.number < 0 ||
reveal.location.transfer_type != DbLocationTransferType.transferred
)
continue;
const brc20 = brc20FromInscriptionContent(
hexToBuffer(reveal.inscription.content as string).toString('utf-8')
);
const brc20 = brc20FromInscription(reveal);
if (brc20) {
switch (brc20.op) {
case 'deploy':
Expand Down Expand Up @@ -208,7 +199,7 @@ export class Brc20PgStore extends BasePgStoreModule {

private async insertDeploy(deploy: {
brc20: Brc20Deploy;
reveal: InscriptionEventData;
reveal: InscriptionRevealData;
pointer: DbLocationPointerInsert;
}): Promise<void> {
if (deploy.reveal.location.transfer_type != DbLocationTransferType.transferred) return;
Expand All @@ -218,10 +209,11 @@ export class Brc20PgStore extends BasePgStoreModule {
tx_id: deploy.reveal.location.tx_id,
address: deploy.pointer.address as string,
ticker: deploy.brc20.tick,
max: deploy.brc20.max,
max: deploy.brc20.max === '0' ? UINT64_MAX.toString() : deploy.brc20.max,
limit: deploy.brc20.lim ?? null,
decimals: deploy.brc20.dec ?? '18',
tx_count: 1,
self_mint: deploy.brc20.self_mint === 'true',
};
const deployRes = await this.sql`
WITH deploy_insert AS (
Expand Down Expand Up @@ -258,19 +250,21 @@ export class Brc20PgStore extends BasePgStoreModule {

private async insertMint(mint: {
brc20: Brc20Mint;
reveal: InscriptionEventData;
reveal: InscriptionRevealData;
pointer: DbLocationPointerInsert;
}): Promise<void> {
if (mint.reveal.location.transfer_type != DbLocationTransferType.transferred) return;
// Check the following conditions:
// * Is the mint amount within the allowed token limits?
// * Is this a self_mint with the correct parent inscription?
// * Is the number of decimals correct?
// * Does the mint amount exceed remaining supply?
const mintRes = await this.sql`
WITH mint_data AS (
SELECT id, decimals, "limit", max, minted_supply
FROM brc20_deploys
WHERE ticker_lower = LOWER(${mint.brc20.tick}) AND minted_supply < max
SELECT d.id, d.decimals, d."limit", d.max, d.minted_supply, d.self_mint, i.genesis_id
FROM brc20_deploys d
INNER JOIN inscriptions i ON i.id = d.inscription_id
WHERE d.ticker_lower = LOWER(${mint.brc20.tick}) AND d.minted_supply < d.max
),
validated_mint AS (
SELECT
Expand All @@ -279,6 +273,10 @@ export class Brc20PgStore extends BasePgStoreModule {
FROM mint_data
WHERE ("limit" IS NULL OR ${mint.brc20.amt}::numeric <= "limit")
AND (SCALE(${mint.brc20.amt}::numeric) <= decimals)
AND (
self_mint = FALSE OR
(self_mint = TRUE AND genesis_id = ${mint.reveal.inscription.parent})
)
),
mint_insert AS (
INSERT INTO brc20_mints (inscription_id, brc20_deploy_id, block_height, tx_id, address, amount) (
Expand Down Expand Up @@ -340,20 +338,13 @@ export class Brc20PgStore extends BasePgStoreModule {
pointer: DbLocationPointerInsert;
}): Promise<void> {
if (transfer.reveal.location.transfer_type != DbLocationTransferType.transferred) return;
// Check the following conditions:
// * Do we have enough available balance to do this transfer?
const transferRes = await this.sql`
WITH balance_data AS (
SELECT b.brc20_deploy_id, COALESCE(SUM(b.avail_balance), 0) AS avail_balance
FROM brc20_balances AS b
INNER JOIN brc20_deploys AS d ON b.brc20_deploy_id = d.id
WHERE d.ticker_lower = LOWER(${transfer.brc20.tick})
AND b.address = ${transfer.pointer.address}
GROUP BY b.brc20_deploy_id
),
validated_transfer AS (
SELECT * FROM balance_data
WHERE avail_balance >= ${transfer.brc20.amt}::numeric
WITH validated_transfer AS (
SELECT brc20_deploy_id, avail_balance
FROM brc20_total_balances
WHERE brc20_deploy_id = (SELECT id FROM brc20_deploys WHERE ticker_lower = LOWER(${transfer.brc20.tick}))
AND address = ${transfer.pointer.address}
AND avail_balance >= ${transfer.brc20.amt}::numeric
),
transfer_insert AS (
INSERT INTO brc20_transfers (inscription_id, brc20_deploy_id, block_height, tx_id, from_address, to_address, amount) (
Expand Down
43 changes: 29 additions & 14 deletions src/pg/brc20/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Static, Type } from '@fastify/type-provider-typebox';
import { TypeCompiler } from '@sinclair/typebox/compiler';
import BigNumber from 'bignumber.js';
import { hexToBuffer } from '../../api/util/helpers';
import { InscriptionData } from '../types';
import { DbLocationTransferType, InscriptionRevealData } from '../types';

const Brc20TickerSchema = Type.String({ minLength: 1 });
const Brc20NumberSchema = Type.RegEx(/^((\d+)|(\d*\.?\d+))$/);
Expand All @@ -15,6 +15,7 @@ const Brc20DeploySchema = Type.Object(
max: Brc20NumberSchema,
lim: Type.Optional(Brc20NumberSchema),
dec: Type.Optional(Type.RegEx(/^\d+$/)),
self_mint: Type.Optional(Type.Literal('true')),
},
{ additionalProperties: true }
);
Expand Down Expand Up @@ -46,28 +47,42 @@ const Brc20Schema = Type.Union([Brc20DeploySchema, Brc20MintSchema, Brc20Transfe
const Brc20C = TypeCompiler.Compile(Brc20Schema);
export type Brc20 = Static<typeof Brc20Schema>;

const UINT64_MAX = BigNumber('18446744073709551615'); // 20 digits
export const UINT64_MAX = BigNumber('18446744073709551615'); // 20 digits
// Only compare against `UINT64_MAX` if the number is at least the same number of digits.
const numExceedsMax = (num: string) => num.length >= 20 && UINT64_MAX.isLessThan(num);

// For testing only
export function brc20FromInscription(inscription: InscriptionData): Brc20 | undefined {
if (inscription.number < 0) return;
if (inscription.mime_type !== 'text/plain' && inscription.mime_type !== 'application/json')
return;
const buf = hexToBuffer(inscription.content as string).toString('utf-8');
return brc20FromInscriptionContent(buf);
}
/**
* Activation block height for
* https://l1f.discourse.group/t/brc-20-proposal-for-issuance-and-burn-enhancements-brc20-ip-1/621/1
*/
export const BRC20_SELF_MINT_ACTIVATION_BLOCK = 837090;

export function brc20FromInscriptionContent(content: string): Brc20 | undefined {
export function brc20FromInscription(reveal: InscriptionRevealData): Brc20 | undefined {
if (
reveal.inscription.classic_number < 0 ||
reveal.inscription.number < 0 ||
reveal.location.transfer_type != DbLocationTransferType.transferred ||
!['text/plain', 'application/json'].includes(reveal.inscription.mime_type)
)
return;
try {
const json = JSON.parse(content);
const json = JSON.parse(hexToBuffer(reveal.inscription.content as string).toString('utf-8'));
if (Brc20C.Check(json)) {
// Check ticker byte length
if (Buffer.from(json.tick).length !== 4) return;
const tick = Buffer.from(json.tick);
if (json.op === 'deploy') {
if (
tick.length === 5 &&
(reveal.location.block_height < BRC20_SELF_MINT_ACTIVATION_BLOCK ||
json.self_mint !== 'true')
)
return;
}
if (tick.length < 4 || tick.length > 5) return;
// Check numeric values.
if (json.op === 'deploy') {
if (parseFloat(json.max) == 0 || numExceedsMax(json.max)) return;
if ((parseFloat(json.max) == 0 && json.self_mint !== 'true') || numExceedsMax(json.max))
return;
if (json.lim && (parseFloat(json.lim) == 0 || numExceedsMax(json.lim))) return;
if (json.dec && parseFloat(json.dec) > 18) return;
} else {
Expand Down
3 changes: 3 additions & 0 deletions src/pg/brc20/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type DbBrc20DeployInsert = {
decimals: string;
limit: string | null;
tx_count: number;
self_mint: boolean;
};

export type DbBrc20MintInsert = {
Expand Down Expand Up @@ -78,6 +79,7 @@ export type DbBrc20Token = {
timestamp: number;
minted_supply: string;
tx_count: string;
self_mint: boolean;
};

export type DbBrc20TokenWithSupply = DbBrc20Token & {
Expand Down Expand Up @@ -188,6 +190,7 @@ export const BRC20_DEPLOYS_COLUMNS = [
'limit',
'minted_supply',
'tx_count',
'self_mint',
];

export const BRC20_TRANSFERS_COLUMNS = [
Expand Down
48 changes: 18 additions & 30 deletions src/pg/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { PgBytea, toEnumValue } from '@hirosystems/api-toolkit';
import { PgBytea, logger, toEnumValue } from '@hirosystems/api-toolkit';
import { hexToBuffer, normalizedHexString, parseSatPoint } from '../api/util/helpers';
import {
BadPayloadRequestError,
BitcoinEvent,
BitcoinInscriptionRevealed,
BitcoinInscriptionTransferred,
} from '@hirosystems/chainhook-client';
import { ENV } from '../env';
import {
DbLocationTransferType,
InscriptionEventData,
Expand All @@ -15,29 +13,6 @@ import {
} from './types';
import { OrdinalSatoshi } from '../api/util/ordinal-satoshi';

/**
* Check if writing a block would create an inscription number gap
* @param currentNumber - Current max blessed number
* @param newNumbers - New blessed numbers to be inserted
*/
export function assertNoBlockInscriptionGap(args: {
currentNumber: number;
newNumbers: number[];
currentBlockHeight: number;
newBlockHeight: number;
}) {
if (!ENV.INSCRIPTION_GAP_DETECTION_ENABLED) return;
args.newNumbers.sort((a, b) => a - b);
for (let n = 0; n < args.newNumbers.length; n++) {
const curr = args.currentNumber + n;
const next = args.newNumbers[n];
if (next !== curr + 1)
throw new BadPayloadRequestError(
`Block inscription gap detected: Attempting to insert #${next} (${args.newBlockHeight}) but current max is #${curr}. Chain tip is at ${args.currentBlockHeight}.`
);
}
}

/**
* Returns a list of referenced inscription ids from inscription content.
* @param content - Inscription content
Expand Down Expand Up @@ -98,12 +73,25 @@ function updateFromOrdhookInscriptionRevealed(args: {
const satoshi = new OrdinalSatoshi(args.reveal.ordinal_number);
const satpoint = parseSatPoint(args.reveal.satpoint_post_inscription);
const recursive_refs = getInscriptionRecursion(args.reveal.content_bytes);
const contentType = removeNullBytes(args.reveal.content_type);
const content_type = removeNullBytes(args.reveal.content_type);
let transfer_type = DbLocationTransferType.transferred;
if (args.reveal.inscriber_address == null || args.reveal.inscriber_address == '') {
if (args.reveal.inscription_output_value == 0) {
if (args.reveal.inscription_pointer !== 0 && args.reveal.inscription_pointer !== null) {
logger.warn(
`Detected inscription reveal with no address and no output value but a valid pointer ${args.reveal.inscription_id}`
);
}
transfer_type = DbLocationTransferType.spentInFees;
} else {
transfer_type = DbLocationTransferType.burnt;
}
}
return {
inscription: {
genesis_id: args.reveal.inscription_id,
mime_type: contentType.split(';')[0],
content_type: contentType,
mime_type: content_type.split(';')[0],
content_type,
content_length: args.reveal.content_length,
number: args.reveal.inscription_number.jubilee,
classic_number: args.reveal.inscription_number.classic,
Expand Down Expand Up @@ -131,7 +119,7 @@ function updateFromOrdhookInscriptionRevealed(args: {
prev_offset: null,
value: args.reveal.inscription_output_value.toString(),
timestamp: args.timestamp,
transfer_type: DbLocationTransferType.transferred,
transfer_type,
},
recursive_refs,
};
Expand Down
Loading

0 comments on commit 27becac

Please sign in to comment.