From 0b25a0ddd41218f81310f69feb2b887e8569429c Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Wed, 20 Aug 2025 19:34:05 +0000 Subject: [PATCH 01/35] Revert "Checkout haf at 1.27.11 for haf_api_node" This reverts commit d30daba57c4f317188b59a9462e6dc8625c60bd6. --- db/schema.sql | 2 +- haf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/db/schema.sql b/db/schema.sql index 68d0cde..07c7b09 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -167,7 +167,7 @@ DECLARE synchronization_stages hive.application_stages; BEGIN IF NOT hive.app_context_exists('nfttracker_app') THEN - synchronization_stages := ARRAY[( 'MASSIVE_PROCESSING', 101, 10000 ), hive.live_stage()]::hive.application_stages; + synchronization_stages := ARRAY[( 'MASSIVE_PROCESSING', 101, 10000, '3 seconds' ), hive.live_stage()]::hive.application_stages; PERFORM hive.app_create_context( _name => 'nfttracker_app', diff --git a/haf b/haf index 0e2bd7c..86bd70a 160000 --- a/haf +++ b/haf @@ -1 +1 @@ -Subproject commit 0e2bd7c69cc26928665281435165fd3288940db9 +Subproject commit 86bd70a81252ce330489ed2a327e6f2f10964386 -- GitLab From 163a519e96ca1e09a4e73a17c68ea5fb26122b21 Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Wed, 20 Aug 2025 16:09:28 -0400 Subject: [PATCH 02/35] Add CI image builds --- .gitlab-ci.yml | 113 +++++++++++++++++++++++++++++++++++++++++++++++++ .gitmodules | 2 +- 2 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..169dbbe --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,113 @@ +stages: +- build +- publish + +variables: + # Variables required by Common CI jobs + CI_COMMON_JOB_VERSION: "6b9e9e75ec5263939450936a4f8348dfbca3666d" + DOCKER_BUILDER_TAG: "$CI_COMMON_JOB_VERSION" + DOCKER_DIND_TAG: "$CI_COMMON_JOB_VERSION" + IMAGE_REMOVER_TAG: "$CI_COMMON_JOB_VERSION" + # Git configuration + GIT_STRATEGY: clone + GIT_SUBMODULE_STRATEGY: recursive + GIT_DEPTH: 1 + GIT_SUBMODULE_DEPTH: 1 + GIT_SUBMODULE_UPDATE_FLAGS: --jobs 4 + +include: +- template: Workflows/Branch-Pipelines.gitlab-ci.yml +- project: 'hive/common-ci-configuration' + ref: 6b9e9e75ec5263939450936a4f8348dfbca3666d + file: + - '/templates/base.gitlab-ci.yml' + - '/templates/docker_image_jobs.gitlab-ci.yml' + +build_images: + extends: .docker_image_builder_job_template + stage: build + before_script: + - !reference [.docker_image_builder_job_template, before_script] + - | + echo -e "\e[0Ksection_start:$(date +%s):login[collapsed=true]\r\e[0KLogging to Docker registry..." + docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY + echo -e "\e[0Ksection_end:$(date +%s):login\r\e[0K" + script: + - | + # Build and push with commit SHA tag (this creates all three tags for GitLab registry) + $CI_PROJECT_DIR/scripts/ci-helpers/build_instance.sh \ + "$CI_COMMIT_SHORT_SHA" \ + "$CI_PROJECT_DIR" \ + "$CI_REGISTRY_IMAGE" + + # Push only the main image to registry.hive.blog (if credentials are available) + echo "Current branch: $CI_COMMIT_BRANCH" + echo "Is protected: $CI_COMMIT_REF_PROTECTED" + echo "Pipeline source: $CI_PIPELINE_SOURCE" + + if [[ -n "$BLOG_REGISTRY_USER" ]] && [[ -n "$BLOG_REGISTRY_PASSWORD" ]]; then + echo "Pushing to registry.hive.blog..." + docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_SHORT_SHA" "registry.hive.blog/nft_tracker:$CI_COMMIT_SHORT_SHA" + echo "Logging in to registry-upload.hive.blog as user: $BLOG_REGISTRY_USER" + echo "$BLOG_REGISTRY_PASSWORD" | docker login --username "$BLOG_REGISTRY_USER" --password-stdin registry-upload.hive.blog + docker push "registry.hive.blog/nft_tracker:$CI_COMMIT_SHORT_SHA" + else + echo "WARNING: BLOG_REGISTRY_USER or BLOG_REGISTRY_PASSWORD not set." + echo "Skipping push to registry.hive.blog (this is expected for non-protected branches)" + echo "Images are still available in GitLab registry at: registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_SHORT_SHA" + fi + + # If on develop branch, also tag and push as 'develop' + if [[ "$CI_COMMIT_BRANCH" == "develop" ]]; then + echo "Tagging images with 'develop' tag..." + docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_SHORT_SHA" "registry.gitlab.syncad.com/hive/nft_tracker:develop" + docker push "registry.gitlab.syncad.com/hive/nft_tracker:develop" + + # Only push develop tag to registry.hive.blog if we have credentials + if [[ -n "$BLOG_REGISTRY_USER" ]] && [[ -n "$BLOG_REGISTRY_PASSWORD" ]]; then + docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_SHORT_SHA" "registry.hive.blog/nft_tracker:develop" + docker push "registry.hive.blog/nft_tracker:develop" + fi + fi + tags: + - public-runner-docker + +publish_release_images: + extends: .docker_image_builder_job_template + stage: publish + before_script: + - !reference [.docker_image_builder_job_template, before_script] + - | + echo -e "\e[0Ksection_start:$(date +%s):login[collapsed=true]\r\e[0KLogging to Docker registries..." + docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY + echo "$BLOG_REGISTRY_PASSWORD" | docker login --username "$BLOG_REGISTRY_USER" --password-stdin registry-upload.hive.blog + echo -e "\e[0Ksection_end:$(date +%s):login\r\e[0K" + script: + - | + echo "Publishing release images for tag: $CI_COMMIT_TAG" + + # Build images with release tag for GitLab registry (creates all three tags) + $CI_PROJECT_DIR/scripts/ci-helpers/build_instance.sh \ + "$CI_COMMIT_TAG" \ + "$CI_PROJECT_DIR" \ + "$CI_REGISTRY_IMAGE" + + # Push only the main image to registry.hive.blog + docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_TAG" "registry.hive.blog/nft_tracker:$CI_COMMIT_TAG" + docker push "registry.hive.blog/nft_tracker:$CI_COMMIT_TAG" + + # Also tag with 'latest' if this is a release tag + docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_TAG" "registry.gitlab.syncad.com/hive/nft_tracker:latest" + docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_TAG" "registry.hive.blog/nft_tracker:latest" + docker push "registry.gitlab.syncad.com/hive/nft_tracker:latest" + docker push "registry.hive.blog/nft_tracker:latest" + + echo "Successfully published release images with tag: $CI_COMMIT_TAG" + rules: + - if: '$CI_COMMIT_TAG =~ /^v[0-9]+.*/ && $CI_COMMIT_REF_PROTECTED == "true"' + when: on_success + - if: '$CI_COMMIT_TAG =~ /^[0-9]+.*/ && $CI_COMMIT_REF_PROTECTED == "true"' + when: on_success + - when: never + tags: + - public-runner-docker \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index d131a23..3f9daf5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "haf"] path = haf - url = git@gitlab.syncad.com:hive/haf.git + url = ../haf.git -- GitLab From 7fdce2ecfd802dd2b90187bca156b7938912623f Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Wed, 20 Aug 2025 16:54:11 -0400 Subject: [PATCH 03/35] Make installer sort of idempotent --- db/schema.sql | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/db/schema.sql b/db/schema.sql index 07c7b09..44518a7 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -2,16 +2,23 @@ SET ROLE nfttracker_owner; CREATE SCHEMA IF NOT EXISTS nfttracker_app AUTHORIZATION nfttracker_owner; +-- Drop and recreate domains to ensure idempotency +DROP DOMAIN IF EXISTS nfttracker_app.symbol_name CASCADE; CREATE DOMAIN nfttracker_app.symbol_name AS VARCHAR(10); +DROP DOMAIN IF EXISTS nfttracker_app.symbol_namespace CASCADE; CREATE DOMAIN nfttracker_app.symbol_namespace AS VARCHAR(16); +DROP DOMAIN IF EXISTS nfttracker_app.positive_integer CASCADE; CREATE DOMAIN nfttracker_app.positive_integer AS INTEGER CHECK (value > 0); +DROP DOMAIN IF EXISTS nfttracker_app.typename CASCADE; CREATE DOMAIN nfttracker_app.typename AS VARCHAR(255) CHECK (length(value) > 0); +-- Drop and recreate composite type +DROP TYPE IF EXISTS nfttracker_app.symbol CASCADE; CREATE TYPE nfttracker_app.symbol AS ( namespace nfttracker_app.symbol_namespace, name nfttracker_app.symbol_name @@ -48,12 +55,20 @@ BEGIN END; $$ LANGUAGE plpgsql IMMUTABLE STRICT; +-- Drop and recreate cast to ensure idempotency +DROP CAST IF EXISTS (TEXT AS nfttracker_app.symbol); CREATE CAST (TEXT AS nfttracker_app.symbol) WITH FUNCTION nfttracker_app.text_to_symbol(TEXT) AS implicit; +DROP DOMAIN IF EXISTS nfttracker_app.tags CASCADE; CREATE DOMAIN nfttracker_app.tags AS VARCHAR(8)[] CHECK (array_length(value, 1) <= 4); -CREATE TABLE IF NOT EXISTS nfttracker_app.types ( +-- Drop and recreate tables to ensure correct schema +DROP TABLE IF EXISTS nfttracker_app.instances CASCADE; +DROP TABLE IF EXISTS nfttracker_app.authorized_issuers CASCADE; +DROP TABLE IF EXISTS nfttracker_app.types CASCADE; + +CREATE TABLE nfttracker_app.types ( id BIGSERIAL PRIMARY KEY, creator INTEGER NOT NULL, owner INTEGER NOT NULL, @@ -120,13 +135,13 @@ FOR EACH ROW WHEN (NEW.max_count > OLD.max_count) EXECUTE FUNCTION nfttracker_app.prevent_increment_max_count(); -CREATE TABLE IF NOT EXISTS nfttracker_app.authorized_issuers ( +CREATE TABLE nfttracker_app.authorized_issuers ( type_id BIGINT NOT NULL REFERENCES nfttracker_app.types(id) ON DELETE CASCADE, account_id INTEGER NOT NULL, PRIMARY KEY (type_id, account_id) ); -CREATE TABLE IF NOT EXISTS nfttracker_app.instances ( +CREATE TABLE nfttracker_app.instances ( id BIGSERIAL PRIMARY KEY, type_id BIGINT NOT NULL REFERENCES nfttracker_app.types(id), holder INTEGER NOT NULL, @@ -136,7 +151,7 @@ CREATE TABLE IF NOT EXISTS nfttracker_app.instances ( created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL ); -CREATE INDEX IF NOT EXISTS idx_nfts_instances_type_id ON nfttracker_app.instances(type_id); +CREATE INDEX idx_nfts_instances_type_id ON nfttracker_app.instances(type_id); CREATE OR REPLACE FUNCTION nfttracker_app.prevent_soulbound_unset() RETURNS TRIGGER AS $$ @@ -176,4 +191,4 @@ BEGIN _stages => synchronization_stages ); END IF; -END $$; +END $$; \ No newline at end of file -- GitLab From 905af9f5bf9ffd79733bc19fafcddf43a59e018e Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Thu, 21 Aug 2025 10:08:28 -0400 Subject: [PATCH 04/35] Add initial postgrest API, mostly copied from hafbe --- .gitignore | 1 + .gitlab-ci.yml | 40 ++++++++- Dockerfile | 2 +- Dockerfile.rewriter | 33 +++++++ docker/nft_tracker_nginx.conf.template | 41 +++++++++ docker/rewriter_entrypoint.sh | 14 +++ endpoints/endpoint_schema.sql | 117 +++++++++++++++++++++++++ endpoints/get_version.sql | 64 ++++++++++++++ rewrite_rules.conf | 8 ++ scripts/install_app.sh | 9 ++ scripts/openapi_rewrite.sh | 100 +++++++++++++++++++++ 11 files changed, 427 insertions(+), 2 deletions(-) create mode 100644 Dockerfile.rewriter create mode 100644 docker/nft_tracker_nginx.conf.template create mode 100755 docker/rewriter_entrypoint.sh create mode 100644 endpoints/endpoint_schema.sql create mode 100644 endpoints/get_version.sql create mode 100644 rewrite_rules.conf create mode 100755 scripts/openapi_rewrite.sh diff --git a/.gitignore b/.gitignore index 748b066..d6a2381 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ tests/regression/results/*.out +endpoints_openapi diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 169dbbe..a4273fd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -40,6 +40,18 @@ build_images: "$CI_PROJECT_DIR" \ "$CI_REGISTRY_IMAGE" + # Build and push the rewriter image + docker build -t "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_SHORT_SHA" \ + -f Dockerfile.rewriter \ + --build-arg BUILD_TIME="$(date -uIseconds)" \ + --build-arg GIT_COMMIT_SHA="$CI_COMMIT_SHA" \ + --build-arg GIT_CURRENT_BRANCH="$CI_COMMIT_BRANCH" \ + --build-arg GIT_LAST_LOG_MESSAGE="$(git log -1 --pretty=%B)" \ + --build-arg GIT_LAST_COMMITTER="$(git log -1 --pretty='%an <%ae>')" \ + --build-arg GIT_LAST_COMMIT_DATE="$(git log -1 --pretty='%aI')" \ + . + docker push "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_SHORT_SHA" + # Push only the main image to registry.hive.blog (if credentials are available) echo "Current branch: $CI_COMMIT_BRANCH" echo "Is protected: $CI_COMMIT_REF_PROTECTED" @@ -48,9 +60,11 @@ build_images: if [[ -n "$BLOG_REGISTRY_USER" ]] && [[ -n "$BLOG_REGISTRY_PASSWORD" ]]; then echo "Pushing to registry.hive.blog..." docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_SHORT_SHA" "registry.hive.blog/nft_tracker:$CI_COMMIT_SHORT_SHA" + docker tag "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_SHORT_SHA" "registry.hive.blog/nft_tracker/postgrest-rewriter:$CI_COMMIT_SHORT_SHA" echo "Logging in to registry-upload.hive.blog as user: $BLOG_REGISTRY_USER" echo "$BLOG_REGISTRY_PASSWORD" | docker login --username "$BLOG_REGISTRY_USER" --password-stdin registry-upload.hive.blog docker push "registry.hive.blog/nft_tracker:$CI_COMMIT_SHORT_SHA" + docker push "registry.hive.blog/nft_tracker/postgrest-rewriter:$CI_COMMIT_SHORT_SHA" else echo "WARNING: BLOG_REGISTRY_USER or BLOG_REGISTRY_PASSWORD not set." echo "Skipping push to registry.hive.blog (this is expected for non-protected branches)" @@ -61,12 +75,16 @@ build_images: if [[ "$CI_COMMIT_BRANCH" == "develop" ]]; then echo "Tagging images with 'develop' tag..." docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_SHORT_SHA" "registry.gitlab.syncad.com/hive/nft_tracker:develop" + docker tag "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_SHORT_SHA" "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:develop" docker push "registry.gitlab.syncad.com/hive/nft_tracker:develop" + docker push "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:develop" # Only push develop tag to registry.hive.blog if we have credentials if [[ -n "$BLOG_REGISTRY_USER" ]] && [[ -n "$BLOG_REGISTRY_PASSWORD" ]]; then docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_SHORT_SHA" "registry.hive.blog/nft_tracker:develop" + docker tag "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_SHORT_SHA" "registry.hive.blog/nft_tracker/postgrest-rewriter:develop" docker push "registry.hive.blog/nft_tracker:develop" + docker push "registry.hive.blog/nft_tracker/postgrest-rewriter:develop" fi fi tags: @@ -92,15 +110,35 @@ publish_release_images: "$CI_PROJECT_DIR" \ "$CI_REGISTRY_IMAGE" - # Push only the main image to registry.hive.blog + # Build and push the rewriter image with release tag + docker build -t "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_TAG" \ + -f Dockerfile.rewriter \ + --target with_tag \ + --build-arg BUILD_TIME="$(date -uIseconds)" \ + --build-arg GIT_COMMIT_SHA="$CI_COMMIT_SHA" \ + --build-arg GIT_CURRENT_BRANCH="$CI_COMMIT_BRANCH" \ + --build-arg GIT_LAST_LOG_MESSAGE="$(git log -1 --pretty=%B)" \ + --build-arg GIT_LAST_COMMITTER="$(git log -1 --pretty='%an <%ae>')" \ + --build-arg GIT_LAST_COMMIT_DATE="$(git log -1 --pretty='%aI')" \ + --build-arg GIT_COMMIT_TAG="$CI_COMMIT_TAG" \ + . + docker push "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_TAG" + + # Push only the main images to registry.hive.blog docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_TAG" "registry.hive.blog/nft_tracker:$CI_COMMIT_TAG" + docker tag "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_TAG" "registry.hive.blog/nft_tracker/postgrest-rewriter:$CI_COMMIT_TAG" docker push "registry.hive.blog/nft_tracker:$CI_COMMIT_TAG" + docker push "registry.hive.blog/nft_tracker/postgrest-rewriter:$CI_COMMIT_TAG" # Also tag with 'latest' if this is a release tag docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_TAG" "registry.gitlab.syncad.com/hive/nft_tracker:latest" + docker tag "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_TAG" "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:latest" docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_TAG" "registry.hive.blog/nft_tracker:latest" + docker tag "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_TAG" "registry.hive.blog/nft_tracker/postgrest-rewriter:latest" docker push "registry.gitlab.syncad.com/hive/nft_tracker:latest" + docker push "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:latest" docker push "registry.hive.blog/nft_tracker:latest" + docker push "registry.hive.blog/nft_tracker/postgrest-rewriter:latest" echo "Successfully published release images with tag: $CI_COMMIT_TAG" rules: diff --git a/Dockerfile b/Dockerfile index 0d78e0f..b531e8f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,7 +39,7 @@ COPY scripts/install_app.sh /app/scripts/install_app.sh COPY scripts/uninstall_app.sh /app/scripts/uninstall_app.sh COPY scripts/process_blocks.sh /app/scripts/process_blocks.sh COPY db /app/db -# COPY endpoints /app/endpoints +COPY endpoints /app/endpoints COPY docker/scripts/block-processing-healthcheck.sh /app/block-processing-healthcheck.sh COPY docker/scripts/docker-entrypoint.sh /app/docker-entrypoint.sh diff --git a/Dockerfile.rewriter b/Dockerfile.rewriter new file mode 100644 index 0000000..1573fe9 --- /dev/null +++ b/Dockerfile.rewriter @@ -0,0 +1,33 @@ +FROM registry.gitlab.syncad.com/hive/common-ci-configuration/nginx:ecd325dd43aee24562f59195ef51a20fa15514d4 AS without_tag + +COPY docker/nft_tracker_nginx.conf.template /usr/local/openresty/nginx/conf/nginx.conf.template +COPY rewrite_rules.conf /usr/local/openresty/nginx/conf/rewrite_rules.conf +COPY docker/rewriter_entrypoint.sh /entrypoint.sh + +CMD ["/entrypoint.sh"] + +ARG BUILD_TIME +ARG GIT_COMMIT_SHA +ARG GIT_CURRENT_BRANCH +ARG GIT_LAST_LOG_MESSAGE +ARG GIT_LAST_COMMITTER +ARG GIT_LAST_COMMIT_DATE +LABEL org.opencontainers.image.created="$BUILD_TIME" +LABEL org.opencontainers.image.url="https://hive.io/" +LABEL org.opencontainers.image.documentation="https://gitlab.syncad.com/hive/nft_tracker" +LABEL org.opencontainers.image.source="https://gitlab.syncad.com/hive/nft_tracker" +#LABEL org.opencontainers.image.version="${VERSION}" +LABEL org.opencontainers.image.revision="$GIT_COMMIT_SHA" +LABEL org.opencontainers.image.licenses="MIT" +LABEL org.opencontainers.image.ref.name="NFT Tracker" +LABEL org.opencontainers.image.title="NFT Tracker PostgREST URL Rewriter Image" +LABEL org.opencontainers.image.description="Rewrites REST calls to provide more natural REST URLs than PostgREST alone allows" +LABEL io.hive.image.branch="$GIT_CURRENT_BRANCH" +LABEL io.hive.image.commit.log_message="$GIT_LAST_LOG_MESSAGE" +LABEL io.hive.image.commit.author="$GIT_LAST_COMMITTER" +LABEL io.hive.image.commit.date="$GIT_LAST_COMMIT_DATE" + +FROM without_tag AS with_tag + +ARG GIT_COMMIT_TAG +LABEL org.opencontainers.image.version="${GIT_COMMIT_TAG}" \ No newline at end of file diff --git a/docker/nft_tracker_nginx.conf.template b/docker/nft_tracker_nginx.conf.template new file mode 100644 index 0000000..55936fe --- /dev/null +++ b/docker/nft_tracker_nginx.conf.template @@ -0,0 +1,41 @@ +# +# Homepage and endpoints of the NFT Tracker API. +# +worker_processes auto; +error_log /dev/stdout info; +worker_rlimit_nofile 8192; + +events { + worker_connections 4096; +} +http { + access_log /dev/stdout; + server { + listen 0.0.0.0:80 default_server; + server_name _; + + location / { + include rewrite_rules.conf; + # ${REWRITE_LOG} will be replaced by the docker entrypoint script. + # Set REWRITE_LOG=on in the environment to enable rewrite logging, + # otherwise it will remain disabled + ${REWRITE_LOG} + + proxy_pass http://nft-tracker-postgrest:3000; # PostgREST endpoint + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_hide_header Content-Location; + proxy_set_header Connection ""; + proxy_http_version 1.1; + } + } + server { + listen 0.0.0.0:81; + + location /health { + return 204; + } + } +} \ No newline at end of file diff --git a/docker/rewriter_entrypoint.sh b/docker/rewriter_entrypoint.sh new file mode 100755 index 0000000..271dd78 --- /dev/null +++ b/docker/rewriter_entrypoint.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# Default value for REWRITE_LOG is off, unless explicitly set to 'on' +if [ "$REWRITE_LOG" = "on" ]; then + REWRITE_LOG="rewrite_log on;" +else + REWRITE_LOG="# rewrite_log off;" +fi + +# Use sed to replace the placeholder in the nginx template file +sed "s|\${REWRITE_LOG}|$REWRITE_LOG|g" /usr/local/openresty/nginx/conf/nginx.conf.template > /usr/local/openresty/nginx/conf/nginx.conf + +# Start nginx +exec /usr/local/openresty/bin/openresty -g 'daemon off;' \ No newline at end of file diff --git a/endpoints/endpoint_schema.sql b/endpoints/endpoint_schema.sql new file mode 100644 index 0000000..38a8657 --- /dev/null +++ b/endpoints/endpoint_schema.sql @@ -0,0 +1,117 @@ +SET ROLE nfttracker_owner; + +/** openapi +openapi: 3.1.0 +info: + title: NFT Tracker + description: >- + NFT Tracker is an API for managing and tracking NFTs on the Hive blockchain + license: + name: MIT License + url: https://opensource.org/license/mit + version: 0.1.0 +externalDocs: + description: NFT Tracker gitlab repository + url: https://gitlab.syncad.com/hive/nft_tracker +tags: + - name: NFT + description: NFT management operations + - name: Other + description: General API information +servers: + - url: /nft-tracker-api + */ + +DO $__$ + DECLARE + __schema_name VARCHAR; + __swagger_url TEXT; + BEGIN + SHOW SEARCH_PATH INTO __schema_name; + __swagger_url := current_setting('custom.swagger_url', true)::TEXT; + IF __swagger_url IS NULL THEN + __swagger_url := 'localhost'; + END IF; + + CREATE SCHEMA IF NOT EXISTS nfttracker_endpoints AUTHORIZATION nfttracker_owner; + + EXECUTE FORMAT( + 'create or replace function nfttracker_endpoints.root() returns json as $_$ + declare + -- openapi-spec +-- openapi-generated-code-begin + openapi json = $$ +{ + "openapi": "3.1.0", + "info": { + "title": "NFT Tracker", + "description": "NFT Tracker is an API for managing and tracking NFTs on the Hive blockchain", + "license": { + "name": "MIT License", + "url": "https://opensource.org/license/mit" + }, + "version": "0.1.0" + }, + "externalDocs": { + "description": "NFT Tracker gitlab repository", + "url": "https://gitlab.syncad.com/hive/nft_tracker" + }, + "tags": [ + { + "name": "NFT", + "description": "NFT management operations" + }, + { + "name": "Other", + "description": "General API information" + } + ], + "servers": [ + { + "url": "/nft-tracker-api" + } + ], + "paths": { + "/version": { + "get": { + "tags": [ + "Other" + ], + "summary": "Get NFT Tracker''s version", + "description": "Get NFT Tracker''s last commit hash (versions set by hash value).\n\nSQL example\n* `SELECT * FROM nfttracker_endpoints.get_version();`\n\nREST call example\n* `GET ''https://%1$s/nft-tracker-api/version''`\n", + "operationId": "nfttracker_endpoints.get_version", + "responses": { + "200": { + "description": "NFT Tracker version\n\n* Returns `TEXT`\n", + "content": { + "application/json": { + "schema": { + "type": "string" + }, + "example": "c2fed8958584511ef1a66dab3dbac8c40f3518f0" + } + } + }, + "404": { + "description": "App not installed" + } + } + } + } + } +} +$$; +-- openapi-generated-code-end +begin + return openapi; +end +$_$ language plpgsql;' + , __swagger_url); + + -- Grant execute permission on the root function to nfttracker_user + GRANT EXECUTE ON FUNCTION nfttracker_endpoints.root() TO nfttracker_user; + + END +$__$; + +RESET ROLE; \ No newline at end of file diff --git a/endpoints/get_version.sql b/endpoints/get_version.sql new file mode 100644 index 0000000..6f4f28a --- /dev/null +++ b/endpoints/get_version.sql @@ -0,0 +1,64 @@ +SET ROLE nfttracker_owner; + +/** openapi:paths +/version: + get: + tags: + - Other + summary: Get NFT Tracker''s version + description: | + Get NFT Tracker''s last commit hash (versions set by hash value). + + SQL example + * `SELECT * FROM nfttracker_endpoints.get_version();` + + REST call example + * `GET ''https://%1$s/nft-tracker-api/version''` + operationId: nfttracker_endpoints.get_version + responses: + '200': + description: | + NFT Tracker version + + * Returns `TEXT` + content: + application/json: + schema: + type: string + example: c2fed8958584511ef1a66dab3dbac8c40f3518f0 + '404': + description: App not installed + */ +-- openapi-generated-code-begin +DROP FUNCTION IF EXISTS nfttracker_endpoints.get_version; +CREATE OR REPLACE FUNCTION nfttracker_endpoints.get_version() +RETURNS TEXT +-- openapi-generated-code-end +LANGUAGE 'plpgsql' STABLE +AS +$$ +DECLARE + _version TEXT; +BEGIN + -- Set cache headers for version endpoint (doesn't change often) + PERFORM set_config('response.headers', '[{"Cache-Control": "public, max-age=100000"}]', true); + + -- For now, return a placeholder version + -- TODO: This should be populated from a version table during deployment + _version := 'development'; + + -- Check if we have a version table (to be created later) + IF EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = 'nfttracker_app' + AND table_name = 'version') THEN + SELECT git_hash INTO _version FROM nfttracker_app.version LIMIT 1; + END IF; + + RETURN _version; +END +$$; + +-- Grant execute permission to nfttracker_user +GRANT EXECUTE ON FUNCTION nfttracker_endpoints.get_version() TO nfttracker_user; + +RESET ROLE; diff --git a/rewrite_rules.conf b/rewrite_rules.conf new file mode 100644 index 0000000..8d98c60 --- /dev/null +++ b/rewrite_rules.conf @@ -0,0 +1,8 @@ +rewrite ^/version /rpc/get_version break; +# endpoint for get /version + +rewrite ^/$ / break; +# endpoint for openapi spec itself + +rewrite ^/(.*)$ /rpc/$1 break; +# default endpoint for everything else diff --git a/scripts/install_app.sh b/scripts/install_app.sh index a359fb7..c99d0dc 100755 --- a/scripts/install_app.sh +++ b/scripts/install_app.sh @@ -64,5 +64,14 @@ psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../db/builtin_roles psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../db/schema.sql" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../db/nft_actions.sql" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../db/main_loop.sql" + +echo "Installing API endpoints..." +# Install endpoint schema and functions +psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/endpoint_schema.sql" +psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/get_version.sql" + +echo "Granting permissions..." psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -c "SET ROLE nfttracker_owner; GRANT USAGE ON SCHEMA ${NFTTRACKER_SCHEMA} to nfttracker_user;" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -c "SET ROLE nfttracker_owner; GRANT SELECT ON ALL TABLES IN SCHEMA ${NFTTRACKER_SCHEMA} TO nfttracker_user;" +psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -c "SET ROLE nfttracker_owner; GRANT USAGE ON SCHEMA nfttracker_endpoints to nfttracker_user;" +psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -c "SET ROLE nfttracker_owner; GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA nfttracker_endpoints TO nfttracker_user;" diff --git a/scripts/openapi_rewrite.sh b/scripts/openapi_rewrite.sh new file mode 100755 index 0000000..2da60f3 --- /dev/null +++ b/scripts/openapi_rewrite.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +set -e +set -o pipefail + +SCRIPTDIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 || exit 1; pwd -P )" + +haf_dir="$SCRIPTDIR/../haf" +endpoints="endpoints" +rewrite_dir="${endpoints}_openapi" +input_file="rewrite_rules.conf" +temp_output_file=$(mktemp) + +# Default directories with fixed order if none provided +OUTPUT="$SCRIPTDIR/output" + +ENDPOINTS_IN_ORDER=" +../$endpoints/endpoint_schema.sql +../$endpoints/get_version.sql" + +# Function to reverse the lines for nginx rewrite rules +reverse_lines() { + awk ' + BEGIN { + RS = "" + FS = "\n" + } + { + for (i = 1; i <= NF; i++) { + if ($i ~ /^#/) { + comment = $i + } else if ($i ~ /^rewrite/) { + rewrite = $i + } + } + if (NR > 1) { + print "" + } + print comment + print rewrite + }' "$input_file" | tac +} + +# Function to install pip3 +install_pip() { + echo "pip3 is not installed. Installing now..." + # Ensure Python 3 is installed + if ! command -v python3 &> /dev/null; then + echo "Python 3 is not installed. Please install Python 3 first." + exit 1 + fi + # Try to install pip3 + sudo apt-get update + sudo apt-get install -y python3-pip + if ! command -v pip3 &> /dev/null; then + echo "pip3 installation failed. Please install pip3 manually." + exit 1 + fi +} + +# Check if pip3 is installed +if ! command -v pip3 &> /dev/null; then + install_pip +fi + +# Check if deepmerge is installed +if python3 -c "import deepmerge" &> /dev/null; then + echo "deepmerge is already installed." +else + echo "deepmerge is not installed. Installing now..." + pip3 install deepmerge + echo "deepmerge has been installed." +fi + +# Check if jsonpointer is installed +if python3 -c "import jsonpointer" &> /dev/null; then + echo "jsonpointer is already installed." +else + echo "jsonpointer is not installed. Installing now..." + pip3 install jsonpointer + echo "jsonpointer has been installed." +fi + +echo "Using endpoints directories" +echo "$ENDPOINTS_IN_ORDER" + +# run openapi rewrite script +# shellcheck disable=SC2086 +python3 $haf_dir/scripts/process_openapi.py $OUTPUT $ENDPOINTS_IN_ORDER + +# Create rewrite_rules.conf +reverse_lines > "$temp_output_file" +mv "$temp_output_file" "../$input_file" +rm "$input_file" + +# Move rewritten directory to root +rm -rf "$SCRIPTDIR/../$rewrite_dir" +mv "$OUTPUT/../$endpoints" "$SCRIPTDIR/../$rewrite_dir" +rm -rf "$SCRIPTDIR/output" +echo "Rewritten endpoint scripts saved in $rewrite_dir" \ No newline at end of file -- GitLab From 94f6610806c35d7b727324c8ad9da542c8c1f9b4 Mon Sep 17 00:00:00 2001 From: Dan Notestein Date: Thu, 21 Aug 2025 21:02:03 +0000 Subject: [PATCH 05/35] Update submodules: - haf: update-submodules-for-1.27.12rc1 (b077291d37d71d482fd22fa68ab23c905a5c0912) --- haf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/haf b/haf index 86bd70a..b077291 160000 --- a/haf +++ b/haf @@ -1 +1 @@ -Subproject commit 86bd70a81252ce330489ed2a327e6f2f10964386 +Subproject commit b077291d37d71d482fd22fa68ab23c905a5c0912 -- GitLab From bed8136ddf678b5f9e56fd76f9f9872e7e5bc6f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Le=C5=9Bniak?= Date: Wed, 20 Aug 2025 11:00:17 +0200 Subject: [PATCH 06/35] Test modifying properties using multiactions --- .../regression/expected/test_modify_multi.out | 41 +++++++++++++++++++ tests/regression/sql/test_modify_multi.sql | 18 ++++++++ 2 files changed, 59 insertions(+) create mode 100644 tests/regression/expected/test_modify_multi.out create mode 100644 tests/regression/sql/test_modify_multi.sql diff --git a/tests/regression/expected/test_modify_multi.out b/tests/regression/expected/test_modify_multi.out new file mode 100644 index 0000000..0541692 --- /dev/null +++ b/tests/regression/expected/test_modify_multi.out @@ -0,0 +1,41 @@ +-- Check modifying properties of using multiactions +-- Given +CALL insert_nft_ops(block_num=>1, auth=>'alice', ops=>ARRAY[ + nft_register_op(symbol=>'alice/A', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>1), + nft_register_op(symbol=>'alice/B', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>2), + nft_register_op(symbol=>'alice/C', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>3), + nft_modify_op(symbol=>'alice/A', name=>'test-mod', owner=>'bob', max_count=>1), + nft_modify_op(symbol=>'alice/B', OWNER=>'bob', max_count=>4), -- Incrementing max_count should fail + nft_modify_op(symbol=>'alice/C', issuers=>ARRAY['charlie'], max_count=>1) +]); +-- When +CALL nfttracker_sync_blocks(); +NOTICE: Last block processed by application: 0 +NOTICE: Entering application main loop... +WARNING: PROFILE: 'nfttracker_app' ATTACHED stage: 'N/A' block: 0 fork: 1 head block: 9 head fork: 1 +NOTICE: HAF instance is ready. Exiting wait loop. +WARNING: Waiting for next block... +NOTICE: HAF instance is ready. Exiting wait loop. +WARNING: PROFILE: 'nfttracker_app' STAGE_CHANGED from 'N/A' to 'live' after N/A block: 1 fork: 1 head block: 9 head fork: 1 +NOTICE: nfttracker processing block: 1... +WARNING: Error processing action modify in block 1: Cannot increment max_count for symbol alice/B. +NOTICE: nfttracker processed block 1 successfully in _ s + +NOTICE: Blocks limit reached. Exiting application main loop at processed block: 1. +-- Then +SELECT creator, owner, symbol, name, max_count, issuers FROM types_view; + creator | owner | symbol | name | max_count | issuers +---------+-------+--------+----------+-----------+----------- + alice | bob | A | test-mod | 1 | {alice} + alice | alice | B | test | 2 | {alice} + alice | alice | C | test | 1 | {charlie} +(3 rows) + +SELECT updated_at = created_at FROM types_view; + ?column? +---------- + t + t + t +(3 rows) + diff --git a/tests/regression/sql/test_modify_multi.sql b/tests/regression/sql/test_modify_multi.sql new file mode 100644 index 0000000..51758bd --- /dev/null +++ b/tests/regression/sql/test_modify_multi.sql @@ -0,0 +1,18 @@ +-- Check modifying properties of using multiactions + +-- Given +CALL insert_nft_ops(block_num=>1, auth=>'alice', ops=>ARRAY[ + nft_register_op(symbol=>'alice/A', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>1), + nft_register_op(symbol=>'alice/B', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>2), + nft_register_op(symbol=>'alice/C', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>3), + nft_modify_op(symbol=>'alice/A', name=>'test-mod', owner=>'bob', max_count=>1), + nft_modify_op(symbol=>'alice/B', OWNER=>'bob', max_count=>4), -- Incrementing max_count should fail + nft_modify_op(symbol=>'alice/C', issuers=>ARRAY['charlie'], max_count=>1) +]); + +-- When +CALL nfttracker_sync_blocks(); + +-- Then +SELECT creator, owner, symbol, name, max_count, issuers FROM types_view; +SELECT updated_at = created_at FROM types_view; -- GitLab From 65f67ba26f8766551f51617d160da373c430b1f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Le=C5=9Bniak?= Date: Wed, 20 Aug 2025 13:01:03 +0200 Subject: [PATCH 07/35] Check name/max_count before trying to register --- db/nft_actions.sql | 29 +++++++++---------- .../expected/test_register_partial.out | 2 +- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/db/nft_actions.sql b/db/nft_actions.sql index 141a3ed..2a1a3eb 100644 --- a/db/nft_actions.sql +++ b/db/nft_actions.sql @@ -138,14 +138,26 @@ AS $$ DECLARE _symbol nfttracker_app.symbol; _issuers hive.account_name_type[]; + _max_count INT; + _name TEXT; err_msg TEXT; err_constraint TEXT; err_column TEXT; BEGIN _symbol := _json->>'symbol'; + _max_count := _json->>'max_count'; + _name := _json->>'name'; + IF _symbol.namespace <> _account THEN RAISE EXCEPTION '% is disallowed to register NFT types in namespace %', _account, _symbol.namespace; END IF; + IF _max_count IS NOT NULL AND _max_count <= 0 THEN + RAISE EXCEPTION 'Invalid max_count value % for NFT %: must be a positive integer', _json->>'max_count', _json->>'symbol'; + END IF; + IF _name IS NULL OR _name = '' THEN + RAISE EXCEPTION 'Invalid name value "%" for NFT %: must be a non-empty string', _json->>'name', _json->>'symbol'; + END IF; + SELECT array_agg(i) INTO _issuers FROM jsonb_array_elements_text(_json->'issuers') AS i; CALL nfttracker_app.require_account_exists(_json->>'owner'); CALL nfttracker_app.require_accounts_exists(_issuers); @@ -154,10 +166,9 @@ BEGIN SELECT _symbol.name AS symbol_name, j.name, - j.max_count, j.owner, j.issuers - FROM jsonb_to_record(_json) AS j(name text, max_count int, owner hive.account_name_type, issuers hive.account_name_type[]) + FROM jsonb_to_record(_json) AS j(name text, owner hive.account_name_type, issuers hive.account_name_type[]) ), new_type AS ( INSERT INTO nfttracker_app.types( @@ -174,7 +185,7 @@ BEGIN o.id, j.symbol_name, j.name, - j.max_count, + _max_count, b.created_at, b.created_at FROM json_fields AS j @@ -191,18 +202,6 @@ BEGIN WHEN unique_violation THEN GET STACKED DIAGNOSTICS err_msg = MESSAGE_TEXT; RAISE EXCEPTION 'NFT type % already exists', _json->>'symbol' USING DETAIL = err_msg; - WHEN check_violation THEN - GET STACKED DIAGNOSTICS - err_msg = MESSAGE_TEXT, - err_constraint = CONSTRAINT_NAME; - CASE err_constraint - WHEN 'positive_integer_check' THEN - RAISE EXCEPTION 'Invalid max_count value % for NFT %: must be a positive integer', _json->>'max_count', _json->>'symbol' USING DETAIL = err_msg; - WHEN 'typename_check' THEN - RAISE EXCEPTION 'Invalid name value "%" for NFT %: must be a non-empty string', _json->>'name', _json->>'symbol' USING DETAIL = err_msg; - ELSE - RAISE; - END CASE; WHEN not_null_violation THEN GET STACKED DIAGNOSTICS err_msg = MESSAGE_TEXT, diff --git a/tests/regression/expected/test_register_partial.out b/tests/regression/expected/test_register_partial.out index 1f79ffb..cc12cf5 100644 --- a/tests/regression/expected/test_register_partial.out +++ b/tests/regression/expected/test_register_partial.out @@ -16,7 +16,7 @@ NOTICE: HAF instance is ready. Exiting wait loop. WARNING: PROFILE: 'nfttracker_app' STAGE_CHANGED from 'N/A' to 'live' after N/A block: 1 fork: 1 head block: 9 head fork: 1 NOTICE: nfttracker processing block: 1... WARNING: Error processing action register in block 1: Symbol is not specified -WARNING: Error processing action register in block 1: "name" cannot be empty for NFT alice/A +WARNING: Error processing action register in block 1: Invalid name value "" for NFT alice/A: must be a non-empty string WARNING: Error processing action register in block 1: Account does not exist NOTICE: nfttracker processed block 1 successfully in _ s -- GitLab From b450c1f230b177d3684720c641687251deb78f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Le=C5=9Bniak?= Date: Wed, 20 Aug 2025 13:25:47 +0200 Subject: [PATCH 08/35] Check name and max_count on modify --- db/nft_actions.sql | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/db/nft_actions.sql b/db/nft_actions.sql index 2a1a3eb..0bfd982 100644 --- a/db/nft_actions.sql +++ b/db/nft_actions.sql @@ -226,11 +226,23 @@ DECLARE _symbol nfttracker_app.symbol; _count BIGINT; _issuers hive.account_name_type[]; + _max_count INT; + _name TEXT; BEGIN _symbol := _json->>'symbol'; + _max_count := _json->>'max_count'; + _name := _json->>'name'; + IF NOT nfttracker_app.is_owner(_symbol, _account) THEN RAISE EXCEPTION '% is disallowed to modify NFT type %', _account, _json->>'symbol'; END IF; + IF _max_count IS NOT NULL AND _max_count <= 0 THEN + RAISE EXCEPTION 'Invalid max_count value % for NFT %: must be a positive integer', _json->>'max_count', _json->>'symbol'; + END IF; + IF _name IS NOT NULL AND _name = '' THEN + RAISE EXCEPTION 'Invalid name value "%" for NFT %: must be a non-empty string', _json->>'name', _json->>'symbol'; + END IF; + SELECT array_agg(i) INTO _issuers FROM jsonb_array_elements_text(_json->'issuers') AS i; IF _json->>'owner' IS NOT NULL THEN CALL nfttracker_app.require_account_exists(_json->>'owner'); @@ -242,17 +254,15 @@ BEGIN SELECT _symbol.name AS symbol_name, _symbol.namespace AS symbol_namespace, - j.name, - j.max_count, j.owner - FROM jsonb_to_record(_json) AS j(symbol text, name text, max_count int, owner hive.account_name_type) + FROM jsonb_to_record(_json) AS j(symbol text, owner hive.account_name_type) ), update_type AS ( UPDATE nfttracker_app.types AS t SET - name = COALESCE(j.name, t.name), + name = COALESCE(_name, t.name), owner = COALESCE(o.id, t.owner), - max_count = COALESCE(j.max_count, t.max_count), + max_count = COALESCE(_max_count, t.max_count), updated_at = b.created_at FROM json_fields AS j JOIN hafd.blocks AS b ON b.num = _block_num -- GitLab From 50965f07a59328e40def226dc27510697c2c589e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Le=C5=9Bniak?= Date: Wed, 20 Aug 2025 13:56:11 +0200 Subject: [PATCH 09/35] Test checking constraints on modification max_count --- db/nft_actions.sql | 25 ++++++++++ .../expected/test_modify_max_count.out | 50 +++++++++++++++++++ .../regression/sql/test_modify_max_count.sql | 25 ++++++++++ 3 files changed, 100 insertions(+) create mode 100644 tests/regression/expected/test_modify_max_count.out create mode 100644 tests/regression/sql/test_modify_max_count.sql diff --git a/db/nft_actions.sql b/db/nft_actions.sql index 0bfd982..8c8f7db 100644 --- a/db/nft_actions.sql +++ b/db/nft_actions.sql @@ -100,6 +100,28 @@ BEGIN END; $$; +CREATE FUNCTION nfttracker_app.issued_count( + IN _symbol nfttracker_app.symbol +) +RETURNS INT +LANGUAGE plpgsql +STABLE +AS +$$ +DECLARE + _issued_count INT; +BEGIN + SELECT COUNT(*) INTO _issued_count + FROM nfttracker_app.instances + WHERE type_id = ( + SELECT id + FROM nfttracker_app.types + WHERE symbol = _symbol.name + AND creator = (SELECT id FROM hafd.accounts WHERE name = _symbol.namespace)); + RETURN _issued_count; +END; +$$; + CREATE OR REPLACE PROCEDURE nfttracker_app.require_account_exists( IN _account hive.account_name_type ) @@ -239,6 +261,9 @@ BEGIN IF _max_count IS NOT NULL AND _max_count <= 0 THEN RAISE EXCEPTION 'Invalid max_count value % for NFT %: must be a positive integer', _json->>'max_count', _json->>'symbol'; END IF; + IF _max_count IS NOT NULL AND _max_count < nfttracker_app.issued_count(_symbol) THEN + RAISE EXCEPTION 'Invalid max_count value % for NFT %: cannot be less than number of issued NFTs', _json->>'max_count', _json->>'symbol'; + END IF; IF _name IS NOT NULL AND _name = '' THEN RAISE EXCEPTION 'Invalid name value "%" for NFT %: must be a non-empty string', _json->>'name', _json->>'symbol'; END IF; diff --git a/tests/regression/expected/test_modify_max_count.out b/tests/regression/expected/test_modify_max_count.out new file mode 100644 index 0000000..c2c3b28 --- /dev/null +++ b/tests/regression/expected/test_modify_max_count.out @@ -0,0 +1,50 @@ +-- Check modifying max_count respect constraints +-- Given +CALL insert_nft_ops(block_num=>1, auth=>'alice', ops=>ARRAY[ + nft_register_op(symbol=>'alice/A', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>1), + nft_register_op(symbol=>'alice/B', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>2), + nft_register_op(symbol=>'alice/C', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>3), + nft_register_op(symbol=>'alice/D', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>4) +]); +CALL insert_nft_ops(block_num=>2, auth=>'alice', ops=>ARRAY[ + nft_issue_op(symbol=>'alice/D', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE), + nft_issue_op(symbol=>'alice/D', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE) +]); +CALL insert_nft_ops(block_num=>3, auth=>'alice', ops=>ARRAY[ + nft_modify_op(symbol=>'alice/A', max_count=>0), + nft_modify_op(symbol=>'alice/B', max_count=>-1), + nft_modify_op(symbol=>'alice/C', max_count=>NULL), + nft_modify_op(symbol=>'alice/D', max_count=>1) +]); +-- When +CALL nfttracker_sync_blocks(); +NOTICE: Last block processed by application: 0 +NOTICE: Entering application main loop... +WARNING: PROFILE: 'nfttracker_app' ATTACHED stage: 'N/A' block: 0 fork: 1 head block: 9 head fork: 1 +NOTICE: HAF instance is ready. Exiting wait loop. +WARNING: Waiting for next block... +NOTICE: HAF instance is ready. Exiting wait loop. +WARNING: PROFILE: 'nfttracker_app' STAGE_CHANGED from 'N/A' to 'live' after N/A block: 1 fork: 1 head block: 9 head fork: 1 +NOTICE: nfttracker processing block: 1... +NOTICE: nfttracker processed block 1 successfully in _ s + +NOTICE: nfttracker processing block: 2... +NOTICE: nfttracker processed block 2 successfully in _ s + +NOTICE: nfttracker processing block: 3... +WARNING: Error processing action modify in block 3: Invalid max_count value 0 for NFT alice/A: must be a positive integer +WARNING: Error processing action modify in block 3: Invalid max_count value -1 for NFT alice/B: must be a positive integer +WARNING: Error processing action modify in block 3: Invalid max_count value 1 for NFT alice/D: cannot be less than number of issued NFTs +NOTICE: nfttracker processed block 3 successfully in _ s + +NOTICE: Blocks limit reached. Exiting application main loop at processed block: 3. +-- Then +SELECT creator, owner, symbol, name, max_count, issuers FROM types_view; + creator | owner | symbol | name | max_count | issuers +---------+-------+--------+------+-----------+--------- + alice | alice | A | test | 1 | {alice} + alice | alice | B | test | 2 | {alice} + alice | alice | C | test | 3 | {alice} + alice | alice | D | test | 4 | {alice} +(4 rows) + diff --git a/tests/regression/sql/test_modify_max_count.sql b/tests/regression/sql/test_modify_max_count.sql new file mode 100644 index 0000000..886bb97 --- /dev/null +++ b/tests/regression/sql/test_modify_max_count.sql @@ -0,0 +1,25 @@ +-- Check modifying max_count respect constraints + +-- Given +CALL insert_nft_ops(block_num=>1, auth=>'alice', ops=>ARRAY[ + nft_register_op(symbol=>'alice/A', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>1), + nft_register_op(symbol=>'alice/B', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>2), + nft_register_op(symbol=>'alice/C', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>3), + nft_register_op(symbol=>'alice/D', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>4) +]); +CALL insert_nft_ops(block_num=>2, auth=>'alice', ops=>ARRAY[ + nft_issue_op(symbol=>'alice/D', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE), + nft_issue_op(symbol=>'alice/D', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE) +]); +CALL insert_nft_ops(block_num=>3, auth=>'alice', ops=>ARRAY[ + nft_modify_op(symbol=>'alice/A', max_count=>0), + nft_modify_op(symbol=>'alice/B', max_count=>-1), + nft_modify_op(symbol=>'alice/C', max_count=>NULL), + nft_modify_op(symbol=>'alice/D', max_count=>1) +]); + +-- When +CALL nfttracker_sync_blocks(); + +-- Then +SELECT creator, owner, symbol, name, max_count, issuers FROM types_view; -- GitLab From 45ba9ccc9ef80c556255f034f628ff2a609fc0f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Le=C5=9Bniak?= Date: Mon, 25 Aug 2025 15:35:08 +0200 Subject: [PATCH 10/35] Basic Postgrest API for NFT tracker --- endpoints/endpoint_schema.sql | 180 ++++++++++++++++++++++++++++++- endpoints/get_nft_instances.sql | 92 ++++++++++++++++ endpoints/get_nft_types.sql | 74 +++++++++++++ endpoints/types/nft_instance.sql | 43 ++++++++ endpoints/types/nft_type.sql | 50 +++++++++ rewrite_rules.conf | 6 ++ scripts/openapi_rewrite.sh | 8 +- 7 files changed, 450 insertions(+), 3 deletions(-) create mode 100644 endpoints/get_nft_instances.sql create mode 100644 endpoints/get_nft_types.sql create mode 100644 endpoints/types/nft_instance.sql create mode 100644 endpoints/types/nft_type.sql diff --git a/endpoints/endpoint_schema.sql b/endpoints/endpoint_schema.sql index 38a8657..8517c1e 100644 --- a/endpoints/endpoint_schema.sql +++ b/endpoints/endpoint_schema.sql @@ -34,6 +34,7 @@ DO $__$ END IF; CREATE SCHEMA IF NOT EXISTS nfttracker_endpoints AUTHORIZATION nfttracker_owner; + CREATE SCHEMA IF NOT EXISTS nfttracker_backend AUTHORIZATION nfttracker_owner; EXECUTE FORMAT( 'create or replace function nfttracker_endpoints.root() returns json as $_$ @@ -42,6 +43,94 @@ DO $__$ -- openapi-generated-code-begin openapi json = $$ { + "components": { + "schemas": { + "nfttracker_backend.nft_type": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "id of NFT type" + }, + "creator": { + "type": "string", + "description": "account name that registered NFT type" + }, + "owner": { + "type": "string", + "description": "current owner of NFT type" + }, + "symbol": { + "type": "string", + "description": "symbol name of registered NFT type" + }, + "name": { + "type": "string", + "description": "name of registered NFT type" + }, + "max_count": { + "type": "integer", + "description": "max number of possible issued instances of this NFT type" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "the timestamp when the NFT type was registered" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "the timestamp when the NFT type was last modified" + }, + "authorized_issuers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "list of accounts that can issue instance of this NFT type" + } + } + }, + "nfttracker_backend.nft_instance": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "id of NFT instance" + }, + "holder": { + "type": "string", + "description": "account currently owning this instance" + }, + "data": { + "type": "string", + "description": "extra data as JSON" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "extra tags associated with this instance" + }, + "soulbound": { + "type": "boolean", + "description": "whether this instance is soulbound (cannot be transferred to other account)" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "the timestamp when this instance was created" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "the timestamp this instance was last modified" + } + } + } + } + }, "openapi": "3.1.0", "info": { "title": "NFT Tracker", @@ -97,6 +186,95 @@ DO $__$ } } } + }, + "/nfts/{creator}/{symbol}": { + "get": { + "tags": [ + "NFT" + ], + "summary": "NFT instances", + "description": "Returns issued instances of given NFT symbol.\n\nSQL example\n* `SELECT * FROM nfttracker_endpoints.get_nft_instances(''alice'', ''TEST'');`\n\nREST call example\n* `GET ''https://%1$s/nfts-api/nfts/alice/TEST''`\n", + "operationId": "nfttracker_endpoints.get_nft_instances", + "parameters": [ + { + "in": "path", + "name": "creator", + "required": true, + "schema": { + "type": "string" + }, + "description": "name of the account that created the NFT type" + }, + { + "in": "path", + "name": "symbol", + "required": true, + "schema": { + "type": "string" + }, + "description": "NFT symbol" + } + ], + "responses": { + "200": { + "description": "Issued NFT instances of given symbol\n\n* Returns `nfttracker_backend.nft_instance`\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/nfttracker_backend.nft_instance" + }, + "example": { + "id": 1, + "holder": "alice", + "data": "{\"key\": \"value\"}", + "tags": [ + "item", + "collectible" + ], + "soulbound": false, + "created_at": "2025-08-22T12:00:00", + "updated_at": "2025-08-22T12:00:00" + } + } + } + }, + "404": { + "description": "creator/symbol combination does not exist\n" + } + } + } + }, + "/nfts": { + "get": { + "tags": [ + "NFT" + ], + "summary": "NFT types", + "description": "Returns registered NFT types.\n\nSQL example\n* `SELECT * FROM nfttracker_endpoints.get_nft_types();`\n\nREST call example\n* `GET ''https://%1$s/nfts-api/nfts''`\n", + "operationId": "nfttracker_endpoints.get_nft_types", + "responses": { + "200": { + "description": "Registered NFT types\n\n* Returns `nfttracker_backend.nft_type`\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/nfttracker_backend.nft_type" + }, + "example": { + "id": 1, + "creator": "alice", + "owner": "bob", + "symbol": "TEST", + "name": "Test symbol", + "max_count": 10, + "created_at": "2025-08-22T12:00:00", + "updated_at": "2025-08-22T12:00:00" + } + } + } + } + } + } } } } @@ -114,4 +292,4 @@ $_$ language plpgsql;' END $__$; -RESET ROLE; \ No newline at end of file +RESET ROLE; diff --git a/endpoints/get_nft_instances.sql b/endpoints/get_nft_instances.sql new file mode 100644 index 0000000..f0593ec --- /dev/null +++ b/endpoints/get_nft_instances.sql @@ -0,0 +1,92 @@ +SET ROLE nft_owner; + +/** openapi:paths +/nfts/{creator}/{symbol}: + get: + tags: + - NFT + summary: NFT instances + description: | + Returns issued instances of given NFT symbol. + + SQL example + * `SELECT * FROM nfttracker_endpoints.get_nft_instances(''alice'', ''TEST'');` + + REST call example + * `GET ''https://%1$s/nfts-api/nfts/alice/TEST''` + operationId: nfttracker_endpoints.get_nft_instances + parameters: + - in: path + name: creator + required: true + schema: + type: string + description: name of the account that created the NFT type + - in: path + name: symbol + required: true + schema: + type: string + description: NFT symbol + responses: + '200': + description: | + Issued NFT instances of given symbol + + * Returns `nfttracker_backend.nft_instance` + content: + application/json: + schema: + $ref: '#/components/schemas/nfttracker_backend.nft_instance' + example: { + "id": 1, + "holder": "alice", + "data": "{\"key\": \"value\"}", + "tags": ["item", "collectible"], + "soulbound": false, + "created_at": "2025-08-22T12:00:00", + "updated_at": "2025-08-22T12:00:00" + } + '404': + description: | + creator/symbol combination does not exist + */ +-- openapi-generated-code-begin +DROP FUNCTION IF EXISTS nfttracker_endpoints.get_nft_instances; +CREATE OR REPLACE FUNCTION nfttracker_endpoints.get_nft_instances( + "creator" TEXT, + "symbol" TEXT +) +RETURNS nfttracker_backend.nft_instance +-- openapi-generated-code-end +LANGUAGE 'plpgsql' STABLE +AS +$$ +DECLARE + _creator TEXT := creator; + _symbol TEXT := symbol; +BEGIN + PERFORM set_config('response.headers', '[{"Cache-Control": "public, max-age=2"}]', true); + + RETURN ( + SELECT ROW( + i.id, + h.name, + i.data, + i.tags, + i.soulbound, + i.created_at, + i.updated_at + )::nfttracker_backend.nft_instance + FROM nfttracker_app.instances AS i + INNER JOIN nfttracker_app.types AS t ON i.type_id = t.id + LEFT JOIN hafd.accounts AS h ON i.holder = h.id + WHERE t.symbol = _symbol + AND t.creator = (SELECT id FROM hafd.accounts WHERE name = _creator) + ORDER BY t.id + LIMIT 1 + ); +END +$$; + +RESET ROLE; diff --git a/endpoints/get_nft_types.sql b/endpoints/get_nft_types.sql new file mode 100644 index 0000000..6f2883e --- /dev/null +++ b/endpoints/get_nft_types.sql @@ -0,0 +1,74 @@ +SET ROLE nft_owner; + +/** openapi:paths +/nfts: + get: + tags: + - NFT + summary: NFT types + description: | + Returns registered NFT types. + + SQL example + * `SELECT * FROM nfttracker_endpoints.get_nft_types();` + + REST call example + * `GET ''https://%1$s/nfts-api/nfts''` + operationId: nfttracker_endpoints.get_nft_types + responses: + '200': + description: | + Registered NFT types + + * Returns `nfttracker_backend.nft_type` + content: + application/json: + schema: + $ref: '#/components/schemas/nfttracker_backend.nft_type' + example: { + "id": 1, + "creator": "alice", + "owner": "bob", + "symbol": "TEST", + "name": "Test symbol", + "max_count": 10, + "created_at": "2025-08-22T12:00:00", + "updated_at": "2025-08-22T12:00:00" + } + */ +-- openapi-generated-code-begin +DROP FUNCTION IF EXISTS nfttracker_endpoints.get_nft_types; +CREATE OR REPLACE FUNCTION nfttracker_endpoints.get_nft_types() +RETURNS nfttracker_backend.nft_type +-- openapi-generated-code-end +LANGUAGE 'plpgsql' STABLE +AS +$$ +BEGIN + PERFORM set_config('response.headers', '[{"Cache-Control": "public, max-age=2"}]', true); + + RETURN ( + SELECT ROW( + t.id, + c.name, + o.name, + t.symbol, + t.name, + t.max_count, + t.created_at, + t.updated_at, + ARRAY_AGG(DISTINCT a.name) FILTER (WHERE a.name IS NOT NULL) + )::nfttracker_backend.nft_type + FROM nfttracker_app.types AS t + LEFT JOIN nfttracker_app.authorized_issuers AS ai ON t.id = ai.type_id + LEFT JOIN hafd.accounts AS a ON ai.account_id = a.id + LEFT JOIN hafd.accounts AS c ON t.creator = c.id + LEFT JOIN hafd.accounts AS o ON t.owner = o.id + GROUP BY t.id, c.name, o.name, t.symbol, t.name, t.max_count, t.created_at, t.updated_at + ORDER BY t.id + LIMIT 1 + ); +END +$$; + +RESET ROLE; diff --git a/endpoints/types/nft_instance.sql b/endpoints/types/nft_instance.sql new file mode 100644 index 0000000..ece8954 --- /dev/null +++ b/endpoints/types/nft_instance.sql @@ -0,0 +1,43 @@ +/** openapi:components:schemas +nfttracker_backend.nft_instance: + type: object + properties: + id: + type: integer + description: id of NFT instance + holder: + type: string + description: account currently owning this instance + data: + type: string + description: extra data as JSON + tags: + type: array + items: + type: string + description: extra tags associated with this instance + soulbound: + type: boolean + description: whether this instance is soulbound (cannot be + transferred to other account) + created_at: + type: string + format: date-time + description: the timestamp when this instance was created + updated_at: + type: string + format: date-time + description: the timestamp this instance was last modified +*/ +-- openapi-generated-code-begin +DROP TYPE IF EXISTS nfttracker_backend.nft_instance CASCADE; +CREATE TYPE nfttracker_backend.nft_instance AS ( + "id" INT, + "holder" TEXT, + "data" TEXT, + "tags" TEXT[], + "soulbound" BOOLEAN, + "created_at" TIMESTAMP, + "updated_at" TIMESTAMP +); +-- openapi-generated-code-end diff --git a/endpoints/types/nft_type.sql b/endpoints/types/nft_type.sql new file mode 100644 index 0000000..9762928 --- /dev/null +++ b/endpoints/types/nft_type.sql @@ -0,0 +1,50 @@ +/** openapi:components:schemas +nfttracker_backend.nft_type: + type: object + properties: + id: + type: integer + description: id of NFT type + creator: + type: string + description: account name that registered NFT type + owner: + type: string + description: current owner of NFT type + symbol: + type: string + description: symbol name of registered NFT type + name: + type: string + description: name of registered NFT type + max_count: + type: integer + description: max number of possible issued instances of this NFT type + created_at: + type: string + format: date-time + description: the timestamp when the NFT type was registered + updated_at: + type: string + format: date-time + description: the timestamp when the NFT type was last modified + authorized_issuers: + type: array + items: + type: string + description: list of accounts that can issue instance of this NFT type +*/ +-- openapi-generated-code-begin +DROP TYPE IF EXISTS nfttracker_backend.nft_type CASCADE; +CREATE TYPE nfttracker_backend.nft_type AS ( + "id" INT, + "creator" TEXT, + "owner" TEXT, + "symbol" TEXT, + "name" TEXT, + "max_count" INT, + "created_at" TIMESTAMP, + "updated_at" TIMESTAMP, + "authorized_issuers" TEXT[] +); +-- openapi-generated-code-end diff --git a/rewrite_rules.conf b/rewrite_rules.conf index 8d98c60..5e65e2e 100644 --- a/rewrite_rules.conf +++ b/rewrite_rules.conf @@ -1,3 +1,9 @@ +rewrite ^/nfts /rpc/get_nft_types break; +# endpoint for get /nfts + +rewrite ^/nfts/([^/]+)/([^/]+) /rpc/get_nft_instances?creator=$1&symbol=$2 break; +# endpoint for get /nfts/{creator}/{symbol} + rewrite ^/version /rpc/get_version break; # endpoint for get /version diff --git a/scripts/openapi_rewrite.sh b/scripts/openapi_rewrite.sh index 2da60f3..23a01b0 100755 --- a/scripts/openapi_rewrite.sh +++ b/scripts/openapi_rewrite.sh @@ -15,8 +15,12 @@ temp_output_file=$(mktemp) OUTPUT="$SCRIPTDIR/output" ENDPOINTS_IN_ORDER=" +../$endpoints/types/nft_type.sql +../$endpoints/types/nft_instance.sql ../$endpoints/endpoint_schema.sql -../$endpoints/get_version.sql" +../$endpoints/get_version.sql +../$endpoints/get_nft_instances.sql +../$endpoints/get_nft_types.sql" # Function to reverse the lines for nginx rewrite rules reverse_lines() { @@ -97,4 +101,4 @@ rm "$input_file" rm -rf "$SCRIPTDIR/../$rewrite_dir" mv "$OUTPUT/../$endpoints" "$SCRIPTDIR/../$rewrite_dir" rm -rf "$SCRIPTDIR/output" -echo "Rewritten endpoint scripts saved in $rewrite_dir" \ No newline at end of file +echo "Rewritten endpoint scripts saved in $rewrite_dir" -- GitLab From e499858acb0a2a9d343a73381f4e925b67d3f0c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Le=C5=9Bniak?= Date: Mon, 25 Aug 2025 15:35:42 +0200 Subject: [PATCH 11/35] Ignore venv --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d6a2381..0cd9938 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ tests/regression/results/*.out endpoints_openapi +venv/ -- GitLab From a248ab1f45cf1983a59cab75bba54161669a6eed Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Mon, 25 Aug 2025 14:58:04 -0400 Subject: [PATCH 12/35] Add missing files to install script --- scripts/install_app.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/install_app.sh b/scripts/install_app.sh index c99d0dc..c87b889 100755 --- a/scripts/install_app.sh +++ b/scripts/install_app.sh @@ -66,9 +66,15 @@ psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../db/nft_actions.s psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../db/main_loop.sql" echo "Installing API endpoints..." +# Install type definitions first +psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/types/nft_type.sql" +psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/types/nft_instance.sql" + # Install endpoint schema and functions psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/endpoint_schema.sql" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/get_version.sql" +psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/get_nft_types.sql" +psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/get_nft_instances.sql" echo "Granting permissions..." psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -c "SET ROLE nfttracker_owner; GRANT USAGE ON SCHEMA ${NFTTRACKER_SCHEMA} to nfttracker_user;" -- GitLab From ea920446a5803153d21a66a935b280671556ac6d Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Mon, 25 Aug 2025 15:02:07 -0400 Subject: [PATCH 13/35] Change nft_owner to nfttracker_owner --- endpoints/get_nft_instances.sql | 2 +- endpoints/get_nft_types.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/endpoints/get_nft_instances.sql b/endpoints/get_nft_instances.sql index f0593ec..100f129 100644 --- a/endpoints/get_nft_instances.sql +++ b/endpoints/get_nft_instances.sql @@ -1,4 +1,4 @@ -SET ROLE nft_owner; +SET ROLE nfttracker_owner; /** openapi:paths /nfts/{creator}/{symbol}: diff --git a/endpoints/get_nft_types.sql b/endpoints/get_nft_types.sql index 6f2883e..55b2984 100644 --- a/endpoints/get_nft_types.sql +++ b/endpoints/get_nft_types.sql @@ -1,4 +1,4 @@ -SET ROLE nft_owner; +SET ROLE nfttracker_owner; /** openapi:paths /nfts: -- GitLab From ad1e9d9979fe57a6ab97904e7247c32417d96af7 Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Mon, 25 Aug 2025 15:23:34 -0400 Subject: [PATCH 14/35] move types into nfttracker_endpoints --- endpoints/endpoint_schema.sql | 9 ++++----- endpoints/get_nft_instances.sql | 8 ++++---- endpoints/get_nft_types.sql | 8 ++++---- endpoints/types/nft_instance.sql | 6 +++--- endpoints/types/nft_type.sql | 6 +++--- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/endpoints/endpoint_schema.sql b/endpoints/endpoint_schema.sql index 8517c1e..0f2cf9c 100644 --- a/endpoints/endpoint_schema.sql +++ b/endpoints/endpoint_schema.sql @@ -34,7 +34,6 @@ DO $__$ END IF; CREATE SCHEMA IF NOT EXISTS nfttracker_endpoints AUTHORIZATION nfttracker_owner; - CREATE SCHEMA IF NOT EXISTS nfttracker_backend AUTHORIZATION nfttracker_owner; EXECUTE FORMAT( 'create or replace function nfttracker_endpoints.root() returns json as $_$ @@ -45,7 +44,7 @@ DO $__$ { "components": { "schemas": { - "nfttracker_backend.nft_type": { + "nfttracker_endpoints.nft_type": { "type": "object", "properties": { "id": { @@ -91,7 +90,7 @@ DO $__$ } } }, - "nfttracker_backend.nft_instance": { + "nfttracker_endpoints.nft_instance": { "type": "object", "properties": { "id": { @@ -221,7 +220,7 @@ DO $__$ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/nfttracker_backend.nft_instance" + "$ref": "#/components/schemas/nfttracker_endpoints.nft_instance" }, "example": { "id": 1, @@ -258,7 +257,7 @@ DO $__$ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/nfttracker_backend.nft_type" + "$ref": "#/components/schemas/nfttracker_endpoints.nft_type" }, "example": { "id": 1, diff --git a/endpoints/get_nft_instances.sql b/endpoints/get_nft_instances.sql index 100f129..405644c 100644 --- a/endpoints/get_nft_instances.sql +++ b/endpoints/get_nft_instances.sql @@ -33,11 +33,11 @@ SET ROLE nfttracker_owner; description: | Issued NFT instances of given symbol - * Returns `nfttracker_backend.nft_instance` + * Returns `nfttracker_endpoints.nft_instance` content: application/json: schema: - $ref: '#/components/schemas/nfttracker_backend.nft_instance' + $ref: '#/components/schemas/nfttracker_endpoints.nft_instance' example: { "id": 1, "holder": "alice", @@ -57,7 +57,7 @@ CREATE OR REPLACE FUNCTION nfttracker_endpoints.get_nft_instances( "creator" TEXT, "symbol" TEXT ) -RETURNS nfttracker_backend.nft_instance +RETURNS nfttracker_endpoints.nft_instance -- openapi-generated-code-end LANGUAGE 'plpgsql' STABLE AS @@ -77,7 +77,7 @@ BEGIN i.soulbound, i.created_at, i.updated_at - )::nfttracker_backend.nft_instance + )::nfttracker_endpoints.nft_instance FROM nfttracker_app.instances AS i INNER JOIN nfttracker_app.types AS t ON i.type_id = t.id LEFT JOIN hafd.accounts AS h ON i.holder = h.id diff --git a/endpoints/get_nft_types.sql b/endpoints/get_nft_types.sql index 55b2984..d7b25d6 100644 --- a/endpoints/get_nft_types.sql +++ b/endpoints/get_nft_types.sql @@ -20,11 +20,11 @@ SET ROLE nfttracker_owner; description: | Registered NFT types - * Returns `nfttracker_backend.nft_type` + * Returns `nfttracker_endpoints.nft_type` content: application/json: schema: - $ref: '#/components/schemas/nfttracker_backend.nft_type' + $ref: '#/components/schemas/nfttracker_endpoints.nft_type' example: { "id": 1, "creator": "alice", @@ -39,7 +39,7 @@ SET ROLE nfttracker_owner; -- openapi-generated-code-begin DROP FUNCTION IF EXISTS nfttracker_endpoints.get_nft_types; CREATE OR REPLACE FUNCTION nfttracker_endpoints.get_nft_types() -RETURNS nfttracker_backend.nft_type +RETURNS nfttracker_endpoints.nft_type -- openapi-generated-code-end LANGUAGE 'plpgsql' STABLE AS @@ -58,7 +58,7 @@ BEGIN t.created_at, t.updated_at, ARRAY_AGG(DISTINCT a.name) FILTER (WHERE a.name IS NOT NULL) - )::nfttracker_backend.nft_type + )::nfttracker_endpoints.nft_type FROM nfttracker_app.types AS t LEFT JOIN nfttracker_app.authorized_issuers AS ai ON t.id = ai.type_id LEFT JOIN hafd.accounts AS a ON ai.account_id = a.id diff --git a/endpoints/types/nft_instance.sql b/endpoints/types/nft_instance.sql index ece8954..01c1c79 100644 --- a/endpoints/types/nft_instance.sql +++ b/endpoints/types/nft_instance.sql @@ -1,5 +1,5 @@ /** openapi:components:schemas -nfttracker_backend.nft_instance: +nfttracker_endpoints.nft_instance: type: object properties: id: @@ -30,8 +30,8 @@ nfttracker_backend.nft_instance: description: the timestamp this instance was last modified */ -- openapi-generated-code-begin -DROP TYPE IF EXISTS nfttracker_backend.nft_instance CASCADE; -CREATE TYPE nfttracker_backend.nft_instance AS ( +DROP TYPE IF EXISTS nfttracker_endpoints.nft_instance CASCADE; +CREATE TYPE nfttracker_endpoints.nft_instance AS ( "id" INT, "holder" TEXT, "data" TEXT, diff --git a/endpoints/types/nft_type.sql b/endpoints/types/nft_type.sql index 9762928..2445ceb 100644 --- a/endpoints/types/nft_type.sql +++ b/endpoints/types/nft_type.sql @@ -1,5 +1,5 @@ /** openapi:components:schemas -nfttracker_backend.nft_type: +nfttracker_endpoints.nft_type: type: object properties: id: @@ -35,8 +35,8 @@ nfttracker_backend.nft_type: description: list of accounts that can issue instance of this NFT type */ -- openapi-generated-code-begin -DROP TYPE IF EXISTS nfttracker_backend.nft_type CASCADE; -CREATE TYPE nfttracker_backend.nft_type AS ( +DROP TYPE IF EXISTS nfttracker_endpoints.nft_type CASCADE; +CREATE TYPE nfttracker_endpoints.nft_type AS ( "id" INT, "creator" TEXT, "owner" TEXT, -- GitLab From 12a65a6ea2a892dcdfe2e63b77104366ce62d567 Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Mon, 25 Aug 2025 15:46:42 -0400 Subject: [PATCH 15/35] Change 'GET nfts' to return an array --- endpoints/endpoint_schema.sql | 62 +++++++++++++++++++-------------- endpoints/get_nft_instances.sql | 46 +++++++++++++----------- endpoints/get_nft_types.sql | 52 ++++++++++++++------------- 3 files changed, 89 insertions(+), 71 deletions(-) diff --git a/endpoints/endpoint_schema.sql b/endpoints/endpoint_schema.sql index 0f2cf9c..eba0d35 100644 --- a/endpoints/endpoint_schema.sql +++ b/endpoints/endpoint_schema.sql @@ -216,24 +216,29 @@ DO $__$ ], "responses": { "200": { - "description": "Issued NFT instances of given symbol\n\n* Returns `nfttracker_backend.nft_instance`\n", + "description": "Issued NFT instances of given symbol\n\n* Returns `nfttracker_endpoints.nft_instance`\n", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/nfttracker_endpoints.nft_instance" + "type": "array", + "items": { + "$ref": "#/components/schemas/nfttracker_endpoints.nft_instance" + } }, - "example": { - "id": 1, - "holder": "alice", - "data": "{\"key\": \"value\"}", - "tags": [ - "item", - "collectible" - ], - "soulbound": false, - "created_at": "2025-08-22T12:00:00", - "updated_at": "2025-08-22T12:00:00" - } + "example": [ + { + "id": 1, + "holder": "alice", + "data": "{\"key\": \"value\"}", + "tags": [ + "item", + "collectible" + ], + "soulbound": false, + "created_at": "2025-08-22T12:00:00", + "updated_at": "2025-08-22T12:00:00" + } + ] } } }, @@ -253,22 +258,27 @@ DO $__$ "operationId": "nfttracker_endpoints.get_nft_types", "responses": { "200": { - "description": "Registered NFT types\n\n* Returns `nfttracker_backend.nft_type`\n", + "description": "Registered NFT types\n\n* Returns `nfttracker_endpoints.nft_type`\n", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/nfttracker_endpoints.nft_type" + "type": "array", + "items": { + "$ref": "#/components/schemas/nfttracker_endpoints.nft_type" + } }, - "example": { - "id": 1, - "creator": "alice", - "owner": "bob", - "symbol": "TEST", - "name": "Test symbol", - "max_count": 10, - "created_at": "2025-08-22T12:00:00", - "updated_at": "2025-08-22T12:00:00" - } + "example": [ + { + "id": 1, + "creator": "alice", + "owner": "bob", + "symbol": "TEST", + "name": "Test symbol", + "max_count": 10, + "created_at": "2025-08-22T12:00:00", + "updated_at": "2025-08-22T12:00:00" + } + ] } } } diff --git a/endpoints/get_nft_instances.sql b/endpoints/get_nft_instances.sql index 405644c..2b6a820 100644 --- a/endpoints/get_nft_instances.sql +++ b/endpoints/get_nft_instances.sql @@ -37,8 +37,10 @@ SET ROLE nfttracker_owner; content: application/json: schema: - $ref: '#/components/schemas/nfttracker_endpoints.nft_instance' - example: { + type: array + items: + $ref: '#/components/schemas/nfttracker_endpoints.nft_instance' + example: [{ "id": 1, "holder": "alice", "data": "{\"key\": \"value\"}", @@ -46,7 +48,7 @@ SET ROLE nfttracker_owner; "soulbound": false, "created_at": "2025-08-22T12:00:00", "updated_at": "2025-08-22T12:00:00" - } + }] '404': description: | creator/symbol combination does not exist @@ -57,7 +59,7 @@ CREATE OR REPLACE FUNCTION nfttracker_endpoints.get_nft_instances( "creator" TEXT, "symbol" TEXT ) -RETURNS nfttracker_endpoints.nft_instance +RETURNS nfttracker_endpoints.nft_instance[] -- openapi-generated-code-end LANGUAGE 'plpgsql' STABLE AS @@ -68,23 +70,25 @@ DECLARE BEGIN PERFORM set_config('response.headers', '[{"Cache-Control": "public, max-age=2"}]', true); - RETURN ( - SELECT ROW( - i.id, - h.name, - i.data, - i.tags, - i.soulbound, - i.created_at, - i.updated_at - )::nfttracker_endpoints.nft_instance - FROM nfttracker_app.instances AS i - INNER JOIN nfttracker_app.types AS t ON i.type_id = t.id - LEFT JOIN hafd.accounts AS h ON i.holder = h.id - WHERE t.symbol = _symbol - AND t.creator = (SELECT id FROM hafd.accounts WHERE name = _creator) - ORDER BY t.id - LIMIT 1 + RETURN COALESCE( + ARRAY( + SELECT ROW( + i.id, + h.name, + i.data, + i.tags, + i.soulbound, + i.created_at, + i.updated_at + )::nfttracker_endpoints.nft_instance + FROM nfttracker_app.instances AS i + INNER JOIN nfttracker_app.types AS t ON i.type_id = t.id + LEFT JOIN hafd.accounts AS h ON i.holder = h.id + WHERE t.symbol = _symbol + AND t.creator = (SELECT id FROM hafd.accounts WHERE name = _creator) + ORDER BY i.id + ), + ARRAY[]::nfttracker_endpoints.nft_instance[] ); END $$; diff --git a/endpoints/get_nft_types.sql b/endpoints/get_nft_types.sql index d7b25d6..8798f8e 100644 --- a/endpoints/get_nft_types.sql +++ b/endpoints/get_nft_types.sql @@ -24,8 +24,10 @@ SET ROLE nfttracker_owner; content: application/json: schema: - $ref: '#/components/schemas/nfttracker_endpoints.nft_type' - example: { + type: array + items: + $ref: '#/components/schemas/nfttracker_endpoints.nft_type' + example: [{ "id": 1, "creator": "alice", "owner": "bob", @@ -34,12 +36,12 @@ SET ROLE nfttracker_owner; "max_count": 10, "created_at": "2025-08-22T12:00:00", "updated_at": "2025-08-22T12:00:00" - } + }] */ -- openapi-generated-code-begin DROP FUNCTION IF EXISTS nfttracker_endpoints.get_nft_types; CREATE OR REPLACE FUNCTION nfttracker_endpoints.get_nft_types() -RETURNS nfttracker_endpoints.nft_type +RETURNS nfttracker_endpoints.nft_type[] -- openapi-generated-code-end LANGUAGE 'plpgsql' STABLE AS @@ -47,26 +49,28 @@ $$ BEGIN PERFORM set_config('response.headers', '[{"Cache-Control": "public, max-age=2"}]', true); - RETURN ( - SELECT ROW( - t.id, - c.name, - o.name, - t.symbol, - t.name, - t.max_count, - t.created_at, - t.updated_at, - ARRAY_AGG(DISTINCT a.name) FILTER (WHERE a.name IS NOT NULL) - )::nfttracker_endpoints.nft_type - FROM nfttracker_app.types AS t - LEFT JOIN nfttracker_app.authorized_issuers AS ai ON t.id = ai.type_id - LEFT JOIN hafd.accounts AS a ON ai.account_id = a.id - LEFT JOIN hafd.accounts AS c ON t.creator = c.id - LEFT JOIN hafd.accounts AS o ON t.owner = o.id - GROUP BY t.id, c.name, o.name, t.symbol, t.name, t.max_count, t.created_at, t.updated_at - ORDER BY t.id - LIMIT 1 + RETURN COALESCE( + ARRAY( + SELECT ROW( + t.id, + c.name, + o.name, + t.symbol, + t.name, + t.max_count, + t.created_at, + t.updated_at, + ARRAY_AGG(DISTINCT a.name) FILTER (WHERE a.name IS NOT NULL) + )::nfttracker_endpoints.nft_type + FROM nfttracker_app.types AS t + LEFT JOIN nfttracker_app.authorized_issuers AS ai ON t.id = ai.type_id + LEFT JOIN hafd.accounts AS a ON ai.account_id = a.id + LEFT JOIN hafd.accounts AS c ON t.creator = c.id + LEFT JOIN hafd.accounts AS o ON t.owner = o.id + GROUP BY t.id, c.name, o.name, t.symbol, t.name, t.max_count, t.created_at, t.updated_at + ORDER BY t.id + ), + ARRAY[]::nfttracker_endpoints.nft_type[] ); END $$; -- GitLab From 73e7323326a5fb2e19546ca22e1d5845c8c25622 Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Mon, 25 Aug 2025 16:03:31 -0400 Subject: [PATCH 16/35] Fix URL in OpenAPI docs --- scripts/install_app.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/install_app.sh b/scripts/install_app.sh index c87b889..a876a4b 100755 --- a/scripts/install_app.sh +++ b/scripts/install_app.sh @@ -12,6 +12,7 @@ OPTIONS: --postgres-port=PORT PostgreSQL port (default: 5432) --postgres-user=USERNAME PostgreSQL user name (default: haf_admin) --postgres-url=URL PostgreSQL URL (if set, overrides three previous options, empty by default) + --swagger-url=URL Server URL for OpenAPI documentation (default: localhost) --help,-h,-? Displays this help message EOF } @@ -21,6 +22,7 @@ POSTGRES_HOST=${POSTGRES_HOST:-"localhost"} POSTGRES_PORT=${POSTGRES_PORT:-5432} POSTGRES_URL=${POSTGRES_URL:-""} NFTTRACKER_SCHEMA=${NFTTRACKER_SCHEMA:-"nfttracker_app"} +SWAGGER_URL=${SWAGGER_URL:-"localhost"} while [ $# -gt 0 ]; do case "$1" in @@ -39,6 +41,9 @@ while [ $# -gt 0 ]; do --schema=*) NFTTRACKER_SCHEMA="${1#*=}" ;; + --swagger-url=*) + SWAGGER_URL="${1#*=}" + ;; --help|-h|-?) print_help exit 0 @@ -71,7 +76,7 @@ psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/types/ psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/types/nft_instance.sql" # Install endpoint schema and functions -psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/endpoint_schema.sql" +psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -c "SET custom.swagger_url = '$SWAGGER_URL';" -f "$SCRIPTPATH/../endpoints/endpoint_schema.sql" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/get_version.sql" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/get_nft_types.sql" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/get_nft_instances.sql" -- GitLab From 877e7ed90b910780f3db18fd1c1cad9f4dbc0781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Le=C5=9Bniak?= Date: Tue, 26 Aug 2025 13:53:38 +0200 Subject: [PATCH 17/35] Add GIN index ot tags --- db/schema.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/db/schema.sql b/db/schema.sql index 44518a7..ee15c94 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -152,6 +152,7 @@ CREATE TABLE nfttracker_app.instances ( updated_at TIMESTAMP NOT NULL ); CREATE INDEX idx_nfts_instances_type_id ON nfttracker_app.instances(type_id); +CREATE INDEX idx_nfts_instances_tags_gin ON nfttracker_app.instances USING GIN (tags); CREATE OR REPLACE FUNCTION nfttracker_app.prevent_soulbound_unset() RETURNS TRIGGER AS $$ -- GitLab From 5d717a72f09f48f9a97ffec8e2ed71cae776c484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Le=C5=9Bniak?= Date: Tue, 26 Aug 2025 13:53:12 +0200 Subject: [PATCH 18/35] Add tags parameter to instances API --- endpoints/endpoint_schema.sql | 13 +++++++++++++ endpoints/get_nft_instances.sql | 14 +++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/endpoints/endpoint_schema.sql b/endpoints/endpoint_schema.sql index eba0d35..9e34077 100644 --- a/endpoints/endpoint_schema.sql +++ b/endpoints/endpoint_schema.sql @@ -212,6 +212,19 @@ DO $__$ "type": "string" }, "description": "NFT symbol" + }, + { + "in": "path", + "name": "tags", + "required": false, + "schema": { + "default": null, + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Only return instances with these tags" } ], "responses": { diff --git a/endpoints/get_nft_instances.sql b/endpoints/get_nft_instances.sql index 2b6a820..084bddc 100644 --- a/endpoints/get_nft_instances.sql +++ b/endpoints/get_nft_instances.sql @@ -28,6 +28,15 @@ SET ROLE nfttracker_owner; schema: type: string description: NFT symbol + - in: path + name: tags + required: false + schema: + default: NULL + type: array + items: + type: string + description: Only return instances with these tags responses: '200': description: | @@ -57,7 +66,8 @@ SET ROLE nfttracker_owner; DROP FUNCTION IF EXISTS nfttracker_endpoints.get_nft_instances; CREATE OR REPLACE FUNCTION nfttracker_endpoints.get_nft_instances( "creator" TEXT, - "symbol" TEXT + "symbol" TEXT, + "tags" TEXT[] = NULL ) RETURNS nfttracker_endpoints.nft_instance[] -- openapi-generated-code-end @@ -67,6 +77,7 @@ $$ DECLARE _creator TEXT := creator; _symbol TEXT := symbol; + _tags TEXT[] := COALESCE(tags, ARRAY[]::TEXT[]); BEGIN PERFORM set_config('response.headers', '[{"Cache-Control": "public, max-age=2"}]', true); @@ -86,6 +97,7 @@ BEGIN LEFT JOIN hafd.accounts AS h ON i.holder = h.id WHERE t.symbol = _symbol AND t.creator = (SELECT id FROM hafd.accounts WHERE name = _creator) + AND i.tags::TEXT[] @> _tags ORDER BY i.id ), ARRAY[]::nfttracker_endpoints.nft_instance[] -- GitLab From 2b702859ef3e044bf640a19ea480920cce0ea35b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Le=C5=9Bniak?= Date: Tue, 26 Aug 2025 17:19:33 +0200 Subject: [PATCH 19/35] Add support for OR patterns in tags --- endpoints/endpoint_schema.sql | 7 ++--- endpoints/get_nft_instances.sql | 55 +++++++++++++++++---------------- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/endpoints/endpoint_schema.sql b/endpoints/endpoint_schema.sql index 9e34077..5bf641b 100644 --- a/endpoints/endpoint_schema.sql +++ b/endpoints/endpoint_schema.sql @@ -219,12 +219,9 @@ DO $__$ "required": false, "schema": { "default": null, - "type": "array", - "items": { - "type": "string" - } + "type": "string" }, - "description": "Only return instances with these tags" + "description": "Only return instances with tags matching pattern.\nPattern is a pipe-separated list of comma-separated tags.\nExample: `a,b|x,y|z` will match instances with tags ''a'' and ''b'', ''x'' and ''y'', or ''z''.\n" } ], "responses": { diff --git a/endpoints/get_nft_instances.sql b/endpoints/get_nft_instances.sql index 084bddc..658aa26 100644 --- a/endpoints/get_nft_instances.sql +++ b/endpoints/get_nft_instances.sql @@ -33,10 +33,11 @@ SET ROLE nfttracker_owner; required: false schema: default: NULL - type: array - items: - type: string - description: Only return instances with these tags + type: string + description: | + Only return instances with tags matching pattern. + Pattern is a pipe-separated list of comma-separated tags. + Example: `a,b|x,y|z` will match instances with tags ''a'' and ''b'', ''x'' and ''y'', or ''z''. responses: '200': description: | @@ -67,7 +68,7 @@ DROP FUNCTION IF EXISTS nfttracker_endpoints.get_nft_instances; CREATE OR REPLACE FUNCTION nfttracker_endpoints.get_nft_instances( "creator" TEXT, "symbol" TEXT, - "tags" TEXT[] = NULL + "tags" TEXT = NULL ) RETURNS nfttracker_endpoints.nft_instance[] -- openapi-generated-code-end @@ -77,31 +78,31 @@ $$ DECLARE _creator TEXT := creator; _symbol TEXT := symbol; - _tags TEXT[] := COALESCE(tags, ARRAY[]::TEXT[]); + _tags TEXT := NULLIF(tags, ''); BEGIN PERFORM set_config('response.headers', '[{"Cache-Control": "public, max-age=2"}]', true); - RETURN COALESCE( - ARRAY( - SELECT ROW( - i.id, - h.name, - i.data, - i.tags, - i.soulbound, - i.created_at, - i.updated_at - )::nfttracker_endpoints.nft_instance - FROM nfttracker_app.instances AS i - INNER JOIN nfttracker_app.types AS t ON i.type_id = t.id - LEFT JOIN hafd.accounts AS h ON i.holder = h.id - WHERE t.symbol = _symbol - AND t.creator = (SELECT id FROM hafd.accounts WHERE name = _creator) - AND i.tags::TEXT[] @> _tags - ORDER BY i.id - ), - ARRAY[]::nfttracker_endpoints.nft_instance[] - ); + RETURN COALESCE(ARRAY( + SELECT ROW( + i.id, + h.name, + i.data, + i.tags, + i.soulbound, + i.created_at, + i.updated_at + )::nfttracker_endpoints.nft_instance + FROM nfttracker_app.instances AS i + INNER JOIN nfttracker_app.types AS t ON i.type_id = t.id + LEFT JOIN hafd.accounts AS h ON i.holder = h.id + WHERE t.symbol = _symbol + AND t.creator = (SELECT id FROM hafd.accounts WHERE name = _creator) + AND (_tags IS NULL OR i.tags::TEXT[] @> ANY( + SELECT STRING_TO_ARRAY(t, ',') + FROM UNNEST(STRING_TO_ARRAY(_tags, '|')) AS t + )) + ORDER BY i.id + ), ARRAY[]::nfttracker_endpoints.nft_instance[]); END $$; -- GitLab From ab7b3758dcef13b3207abe56a4d266e2b9a52972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Le=C5=9Bniak?= Date: Wed, 27 Aug 2025 13:18:55 +0200 Subject: [PATCH 20/35] Install endpoint_schema.sql before types --- scripts/install_app.sh | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/install_app.sh b/scripts/install_app.sh index a876a4b..60b237d 100755 --- a/scripts/install_app.sh +++ b/scripts/install_app.sh @@ -71,12 +71,10 @@ psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../db/nft_actions.s psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../db/main_loop.sql" echo "Installing API endpoints..." -# Install type definitions first +psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -c "SET custom.swagger_url = '$SWAGGER_URL';" -f "$SCRIPTPATH/../endpoints/endpoint_schema.sql" + psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/types/nft_type.sql" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/types/nft_instance.sql" - -# Install endpoint schema and functions -psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -c "SET custom.swagger_url = '$SWAGGER_URL';" -f "$SCRIPTPATH/../endpoints/endpoint_schema.sql" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/get_version.sql" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/get_nft_types.sql" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/get_nft_instances.sql" -- GitLab From 49f5be0484e28b4ec6e5ee9d0549782ce80f6454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Le=C5=9Bniak?= Date: Thu, 28 Aug 2025 13:05:46 +0200 Subject: [PATCH 21/35] Add url rewrite for getting instances with tags --- endpoints/endpoint_schema.sql | 65 ++++++++++++- endpoints/get_nft_instances.sql | 41 +------- endpoints/get_nft_instances_with_tags.sql | 108 ++++++++++++++++++++++ rewrite_rules.conf | 3 + scripts/install_app.sh | 1 + scripts/openapi_rewrite.sh | 1 + 6 files changed, 178 insertions(+), 41 deletions(-) create mode 100644 endpoints/get_nft_instances_with_tags.sql diff --git a/endpoints/endpoint_schema.sql b/endpoints/endpoint_schema.sql index 5bf641b..eb95d45 100644 --- a/endpoints/endpoint_schema.sql +++ b/endpoints/endpoint_schema.sql @@ -194,6 +194,68 @@ DO $__$ "summary": "NFT instances", "description": "Returns issued instances of given NFT symbol.\n\nSQL example\n* `SELECT * FROM nfttracker_endpoints.get_nft_instances(''alice'', ''TEST'');`\n\nREST call example\n* `GET ''https://%1$s/nfts-api/nfts/alice/TEST''`\n", "operationId": "nfttracker_endpoints.get_nft_instances", + "parameters": [ + { + "in": "path", + "name": "creator", + "required": true, + "schema": { + "type": "string" + }, + "description": "name of the account that created the NFT type" + }, + { + "in": "path", + "name": "symbol", + "required": true, + "schema": { + "type": "string" + }, + "description": "NFT symbol" + } + ], + "responses": { + "200": { + "description": "Issued NFT instances of given symbol\n\n* Returns `nfttracker_endpoints.nft_instance`\n", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/nfttracker_endpoints.nft_instance" + } + }, + "example": [ + { + "id": 1, + "holder": "alice", + "data": "{\"key\": \"value\"}", + "tags": [ + "item", + "collectible" + ], + "soulbound": false, + "created_at": "2025-08-22T12:00:00", + "updated_at": "2025-08-22T12:00:00" + } + ] + } + } + }, + "404": { + "description": "creator/symbol combination does not exist\n" + } + } + } + }, + "/nfts/{creator}/{symbol}/{tags}": { + "get": { + "tags": [ + "NFT" + ], + "summary": "NFT instances", + "description": "Returns issued instances of given NFT symbol.\n\nSQL example\n* `SELECT * FROM nfttracker_endpoints.get_nft_instances(''alice'', ''TEST'');`\n\nREST call example\n* `GET ''https://%1$s/nfts-api/nfts/alice/TEST''`\n", + "operationId": "nfttracker_endpoints.get_nft_instances_with_tags", "parameters": [ { "in": "path", @@ -216,9 +278,8 @@ DO $__$ { "in": "path", "name": "tags", - "required": false, + "required": true, "schema": { - "default": null, "type": "string" }, "description": "Only return instances with tags matching pattern.\nPattern is a pipe-separated list of comma-separated tags.\nExample: `a,b|x,y|z` will match instances with tags ''a'' and ''b'', ''x'' and ''y'', or ''z''.\n" diff --git a/endpoints/get_nft_instances.sql b/endpoints/get_nft_instances.sql index 658aa26..14fe46d 100644 --- a/endpoints/get_nft_instances.sql +++ b/endpoints/get_nft_instances.sql @@ -28,16 +28,6 @@ SET ROLE nfttracker_owner; schema: type: string description: NFT symbol - - in: path - name: tags - required: false - schema: - default: NULL - type: string - description: | - Only return instances with tags matching pattern. - Pattern is a pipe-separated list of comma-separated tags. - Example: `a,b|x,y|z` will match instances with tags ''a'' and ''b'', ''x'' and ''y'', or ''z''. responses: '200': description: | @@ -67,42 +57,15 @@ SET ROLE nfttracker_owner; DROP FUNCTION IF EXISTS nfttracker_endpoints.get_nft_instances; CREATE OR REPLACE FUNCTION nfttracker_endpoints.get_nft_instances( "creator" TEXT, - "symbol" TEXT, - "tags" TEXT = NULL + "symbol" TEXT ) RETURNS nfttracker_endpoints.nft_instance[] -- openapi-generated-code-end LANGUAGE 'plpgsql' STABLE AS $$ -DECLARE - _creator TEXT := creator; - _symbol TEXT := symbol; - _tags TEXT := NULLIF(tags, ''); BEGIN - PERFORM set_config('response.headers', '[{"Cache-Control": "public, max-age=2"}]', true); - - RETURN COALESCE(ARRAY( - SELECT ROW( - i.id, - h.name, - i.data, - i.tags, - i.soulbound, - i.created_at, - i.updated_at - )::nfttracker_endpoints.nft_instance - FROM nfttracker_app.instances AS i - INNER JOIN nfttracker_app.types AS t ON i.type_id = t.id - LEFT JOIN hafd.accounts AS h ON i.holder = h.id - WHERE t.symbol = _symbol - AND t.creator = (SELECT id FROM hafd.accounts WHERE name = _creator) - AND (_tags IS NULL OR i.tags::TEXT[] @> ANY( - SELECT STRING_TO_ARRAY(t, ',') - FROM UNNEST(STRING_TO_ARRAY(_tags, '|')) AS t - )) - ORDER BY i.id - ), ARRAY[]::nfttracker_endpoints.nft_instance[]); + RETURN nfttracker_endpoints.get_nft_instances_with_tags(creator, symbol, NULL); END $$; diff --git a/endpoints/get_nft_instances_with_tags.sql b/endpoints/get_nft_instances_with_tags.sql new file mode 100644 index 0000000..6ab801e --- /dev/null +++ b/endpoints/get_nft_instances_with_tags.sql @@ -0,0 +1,108 @@ +SET ROLE nfttracker_owner; + +/** openapi:paths +/nfts/{creator}/{symbol}/{tags}: + get: + tags: + - NFT + summary: NFT instances + description: | + Returns issued instances of given NFT symbol. + + SQL example + * `SELECT * FROM nfttracker_endpoints.get_nft_instances(''alice'', ''TEST'');` + + REST call example + * `GET ''https://%1$s/nfts-api/nfts/alice/TEST''` + operationId: nfttracker_endpoints.get_nft_instances_with_tags + parameters: + - in: path + name: creator + required: true + schema: + type: string + description: name of the account that created the NFT type + - in: path + name: symbol + required: true + schema: + type: string + description: NFT symbol + - in: path + name: tags + required: true + schema: + type: string + description: | + Only return instances with tags matching pattern. + Pattern is a pipe-separated list of comma-separated tags. + Example: `a,b|x,y|z` will match instances with tags ''a'' and ''b'', ''x'' and ''y'', or ''z''. + responses: + '200': + description: | + Issued NFT instances of given symbol + + * Returns `nfttracker_endpoints.nft_instance` + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/nfttracker_endpoints.nft_instance' + example: [{ + "id": 1, + "holder": "alice", + "data": "{\"key\": \"value\"}", + "tags": ["item", "collectible"], + "soulbound": false, + "created_at": "2025-08-22T12:00:00", + "updated_at": "2025-08-22T12:00:00" + }] + '404': + description: | + creator/symbol combination does not exist + */ +-- openapi-generated-code-begin +DROP FUNCTION IF EXISTS nfttracker_endpoints.get_nft_instances_with_tags; +CREATE OR REPLACE FUNCTION nfttracker_endpoints.get_nft_instances_with_tags( + "creator" TEXT, + "symbol" TEXT, + "tags" TEXT +) +RETURNS nfttracker_endpoints.nft_instance[] +-- openapi-generated-code-end +LANGUAGE 'plpgsql' STABLE +AS +$$ +DECLARE + _creator TEXT := creator; + _symbol TEXT := symbol; + _tags TEXT := NULLIF(tags, ''); +BEGIN + PERFORM set_config('response.headers', '[{"Cache-Control": "public, max-age=2"}]', true); + + RETURN COALESCE(ARRAY( + SELECT ROW( + i.id, + h.name, + i.data, + i.tags, + i.soulbound, + i.created_at, + i.updated_at + )::nfttracker_endpoints.nft_instance + FROM nfttracker_app.instances AS i + INNER JOIN nfttracker_app.types AS t ON i.type_id = t.id + LEFT JOIN hafd.accounts AS h ON i.holder = h.id + WHERE t.symbol = _symbol + AND t.creator = (SELECT id FROM hafd.accounts WHERE name = _creator) + AND (_tags IS NULL OR i.tags::TEXT[] @> ANY( + SELECT STRING_TO_ARRAY(t, ',') + FROM UNNEST(STRING_TO_ARRAY(_tags, '|')) AS t + )) + ORDER BY i.id + ), ARRAY[]::nfttracker_endpoints.nft_instance[]); +END +$$; + +RESET ROLE; diff --git a/rewrite_rules.conf b/rewrite_rules.conf index 5e65e2e..79bb90a 100644 --- a/rewrite_rules.conf +++ b/rewrite_rules.conf @@ -1,6 +1,9 @@ rewrite ^/nfts /rpc/get_nft_types break; # endpoint for get /nfts +rewrite ^/nfts/([^/]+)/([^/]+)/([^/]+) /rpc/get_nft_instances_with_tags?creator=$1&symbol=$2&tags=$3 break; +# endpoint for get /nfts/{creator}/{symbol}/{tags} + rewrite ^/nfts/([^/]+)/([^/]+) /rpc/get_nft_instances?creator=$1&symbol=$2 break; # endpoint for get /nfts/{creator}/{symbol} diff --git a/scripts/install_app.sh b/scripts/install_app.sh index 60b237d..6469962 100755 --- a/scripts/install_app.sh +++ b/scripts/install_app.sh @@ -77,6 +77,7 @@ psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/types/ psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/types/nft_instance.sql" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/get_version.sql" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/get_nft_types.sql" +psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/get_nft_instances_with_tags.sql" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/get_nft_instances.sql" echo "Granting permissions..." diff --git a/scripts/openapi_rewrite.sh b/scripts/openapi_rewrite.sh index 23a01b0..55ea396 100755 --- a/scripts/openapi_rewrite.sh +++ b/scripts/openapi_rewrite.sh @@ -20,6 +20,7 @@ ENDPOINTS_IN_ORDER=" ../$endpoints/endpoint_schema.sql ../$endpoints/get_version.sql ../$endpoints/get_nft_instances.sql +../$endpoints/get_nft_instances_with_tags.sql ../$endpoints/get_nft_types.sql" # Function to reverse the lines for nginx rewrite rules -- GitLab From 80f6eeb33538322867c3631ca3af1f24d897cdaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Le=C5=9Bniak?= Date: Thu, 28 Aug 2025 15:16:34 +0200 Subject: [PATCH 22/35] Fix url rewrites order --- endpoints/endpoint_schema.sql | 74 +++++++++++++++++------------------ rewrite_rules.conf | 6 +-- scripts/openapi_rewrite.sh | 4 +- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/endpoints/endpoint_schema.sql b/endpoints/endpoint_schema.sql index eb95d45..10b49b5 100644 --- a/endpoints/endpoint_schema.sql +++ b/endpoints/endpoint_schema.sql @@ -186,6 +186,43 @@ DO $__$ } } }, + "/nfts": { + "get": { + "tags": [ + "NFT" + ], + "summary": "NFT types", + "description": "Returns registered NFT types.\n\nSQL example\n* `SELECT * FROM nfttracker_endpoints.get_nft_types();`\n\nREST call example\n* `GET ''https://%1$s/nfts-api/nfts''`\n", + "operationId": "nfttracker_endpoints.get_nft_types", + "responses": { + "200": { + "description": "Registered NFT types\n\n* Returns `nfttracker_endpoints.nft_type`\n", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/nfttracker_endpoints.nft_type" + } + }, + "example": [ + { + "id": 1, + "creator": "alice", + "owner": "bob", + "symbol": "TEST", + "name": "Test symbol", + "max_count": 10, + "created_at": "2025-08-22T12:00:00", + "updated_at": "2025-08-22T12:00:00" + } + ] + } + } + } + } + } + }, "/nfts/{creator}/{symbol}": { "get": { "tags": [ @@ -318,43 +355,6 @@ DO $__$ } } } - }, - "/nfts": { - "get": { - "tags": [ - "NFT" - ], - "summary": "NFT types", - "description": "Returns registered NFT types.\n\nSQL example\n* `SELECT * FROM nfttracker_endpoints.get_nft_types();`\n\nREST call example\n* `GET ''https://%1$s/nfts-api/nfts''`\n", - "operationId": "nfttracker_endpoints.get_nft_types", - "responses": { - "200": { - "description": "Registered NFT types\n\n* Returns `nfttracker_endpoints.nft_type`\n", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/nfttracker_endpoints.nft_type" - } - }, - "example": [ - { - "id": 1, - "creator": "alice", - "owner": "bob", - "symbol": "TEST", - "name": "Test symbol", - "max_count": 10, - "created_at": "2025-08-22T12:00:00", - "updated_at": "2025-08-22T12:00:00" - } - ] - } - } - } - } - } } } } diff --git a/rewrite_rules.conf b/rewrite_rules.conf index 79bb90a..984be8c 100644 --- a/rewrite_rules.conf +++ b/rewrite_rules.conf @@ -1,12 +1,12 @@ -rewrite ^/nfts /rpc/get_nft_types break; -# endpoint for get /nfts - rewrite ^/nfts/([^/]+)/([^/]+)/([^/]+) /rpc/get_nft_instances_with_tags?creator=$1&symbol=$2&tags=$3 break; # endpoint for get /nfts/{creator}/{symbol}/{tags} rewrite ^/nfts/([^/]+)/([^/]+) /rpc/get_nft_instances?creator=$1&symbol=$2 break; # endpoint for get /nfts/{creator}/{symbol} +rewrite ^/nfts /rpc/get_nft_types break; +# endpoint for get /nfts + rewrite ^/version /rpc/get_version break; # endpoint for get /version diff --git a/scripts/openapi_rewrite.sh b/scripts/openapi_rewrite.sh index 55ea396..be7708e 100755 --- a/scripts/openapi_rewrite.sh +++ b/scripts/openapi_rewrite.sh @@ -19,9 +19,9 @@ ENDPOINTS_IN_ORDER=" ../$endpoints/types/nft_instance.sql ../$endpoints/endpoint_schema.sql ../$endpoints/get_version.sql +../$endpoints/get_nft_types.sql ../$endpoints/get_nft_instances.sql -../$endpoints/get_nft_instances_with_tags.sql -../$endpoints/get_nft_types.sql" +../$endpoints/get_nft_instances_with_tags.sql" # Function to reverse the lines for nginx rewrite rules reverse_lines() { -- GitLab From ccbe73f53bb7f605bb195d3f160adf00dd5bc0d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Le=C5=9Bniak?= Date: Thu, 28 Aug 2025 16:54:58 +0200 Subject: [PATCH 23/35] Extract nfttracker_backend.get_nft_instances --- endpoints/backend/get_nft_instances.sql | 42 +++++++++++++++++++++++ endpoints/endpoint_schema.sql | 1 + endpoints/get_nft_instances.sql | 4 ++- endpoints/get_nft_instances_with_tags.sql | 26 +------------- scripts/install_app.sh | 2 ++ 5 files changed, 49 insertions(+), 26 deletions(-) create mode 100644 endpoints/backend/get_nft_instances.sql diff --git a/endpoints/backend/get_nft_instances.sql b/endpoints/backend/get_nft_instances.sql new file mode 100644 index 0000000..9210392 --- /dev/null +++ b/endpoints/backend/get_nft_instances.sql @@ -0,0 +1,42 @@ +SET ROLE nfttracker_owner; + +DROP FUNCTION IF EXISTS nfttracker_backend.get_nft_instances; +CREATE OR REPLACE FUNCTION nfttracker_backend.get_nft_instances( + "creator" TEXT, + "symbol" TEXT, + "tags" TEXT +) +RETURNS nfttracker_endpoints.nft_instance[] +LANGUAGE 'plpgsql' STABLE +AS +$$ +DECLARE + _creator TEXT := creator; + _symbol TEXT := symbol; + _tags TEXT := NULLIF(tags, ''); +BEGIN + RETURN COALESCE(ARRAY( + SELECT ROW( + i.id, + h.name, + i.data, + i.tags, + i.soulbound, + i.created_at, + i.updated_at + )::nfttracker_endpoints.nft_instance + FROM nfttracker_app.instances AS i + INNER JOIN nfttracker_app.types AS t ON i.type_id = t.id + LEFT JOIN hafd.accounts AS h ON i.holder = h.id + WHERE t.symbol = _symbol + AND t.creator = (SELECT id FROM hafd.accounts WHERE name = _creator) + AND (_tags IS NULL OR i.tags::TEXT[] @> ANY( + SELECT STRING_TO_ARRAY(t, ',') + FROM UNNEST(STRING_TO_ARRAY(_tags, '|')) AS t + )) + ORDER BY i.id + ), ARRAY[]::nfttracker_endpoints.nft_instance[]); +END +$$; + +RESET ROLE; diff --git a/endpoints/endpoint_schema.sql b/endpoints/endpoint_schema.sql index 10b49b5..7cfc395 100644 --- a/endpoints/endpoint_schema.sql +++ b/endpoints/endpoint_schema.sql @@ -34,6 +34,7 @@ DO $__$ END IF; CREATE SCHEMA IF NOT EXISTS nfttracker_endpoints AUTHORIZATION nfttracker_owner; + CREATE SCHEMA IF NOT EXISTS nfttracker_backend AUTHORIZATION nfttracker_owner; EXECUTE FORMAT( 'create or replace function nfttracker_endpoints.root() returns json as $_$ diff --git a/endpoints/get_nft_instances.sql b/endpoints/get_nft_instances.sql index 14fe46d..f4139d3 100644 --- a/endpoints/get_nft_instances.sql +++ b/endpoints/get_nft_instances.sql @@ -65,7 +65,9 @@ LANGUAGE 'plpgsql' STABLE AS $$ BEGIN - RETURN nfttracker_endpoints.get_nft_instances_with_tags(creator, symbol, NULL); + PERFORM set_config('response.headers', '[{"Cache-Control": "public, max-age=2"}]', true); + + RETURN nfttracker_backend.get_nft_instances(creator, symbol, NULL); END $$; diff --git a/endpoints/get_nft_instances_with_tags.sql b/endpoints/get_nft_instances_with_tags.sql index 6ab801e..6b7c5ab 100644 --- a/endpoints/get_nft_instances_with_tags.sql +++ b/endpoints/get_nft_instances_with_tags.sql @@ -74,34 +74,10 @@ RETURNS nfttracker_endpoints.nft_instance[] LANGUAGE 'plpgsql' STABLE AS $$ -DECLARE - _creator TEXT := creator; - _symbol TEXT := symbol; - _tags TEXT := NULLIF(tags, ''); BEGIN PERFORM set_config('response.headers', '[{"Cache-Control": "public, max-age=2"}]', true); - RETURN COALESCE(ARRAY( - SELECT ROW( - i.id, - h.name, - i.data, - i.tags, - i.soulbound, - i.created_at, - i.updated_at - )::nfttracker_endpoints.nft_instance - FROM nfttracker_app.instances AS i - INNER JOIN nfttracker_app.types AS t ON i.type_id = t.id - LEFT JOIN hafd.accounts AS h ON i.holder = h.id - WHERE t.symbol = _symbol - AND t.creator = (SELECT id FROM hafd.accounts WHERE name = _creator) - AND (_tags IS NULL OR i.tags::TEXT[] @> ANY( - SELECT STRING_TO_ARRAY(t, ',') - FROM UNNEST(STRING_TO_ARRAY(_tags, '|')) AS t - )) - ORDER BY i.id - ), ARRAY[]::nfttracker_endpoints.nft_instance[]); + RETURN nfttracker_backend.get_nft_instances(creator, symbol, tags); END $$; diff --git a/scripts/install_app.sh b/scripts/install_app.sh index 6469962..5916391 100755 --- a/scripts/install_app.sh +++ b/scripts/install_app.sh @@ -75,6 +75,7 @@ psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -c "SET custom.swagger_url = '$SWAGG psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/types/nft_type.sql" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/types/nft_instance.sql" +psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/backend/get_nft_instances.sql" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/get_version.sql" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/get_nft_types.sql" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -f "$SCRIPTPATH/../endpoints/get_nft_instances_with_tags.sql" @@ -84,4 +85,5 @@ echo "Granting permissions..." psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -c "SET ROLE nfttracker_owner; GRANT USAGE ON SCHEMA ${NFTTRACKER_SCHEMA} to nfttracker_user;" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -c "SET ROLE nfttracker_owner; GRANT SELECT ON ALL TABLES IN SCHEMA ${NFTTRACKER_SCHEMA} TO nfttracker_user;" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -c "SET ROLE nfttracker_owner; GRANT USAGE ON SCHEMA nfttracker_endpoints to nfttracker_user;" +psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -c "SET ROLE nfttracker_owner; GRANT USAGE ON SCHEMA nfttracker_backend to nfttracker_user;" psql "$POSTGRES_ACCESS" -v ON_ERROR_STOP=on -c "SET ROLE nfttracker_owner; GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA nfttracker_endpoints TO nfttracker_user;" -- GitLab From b2bb76b134c56f0d58596edc5a0eae2aa3b90487 Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Fri, 29 Aug 2025 10:40:47 -0400 Subject: [PATCH 24/35] Don't push untagged releases to registry.hive.blog. Fix mismatch between registry.hive.blog and registry-upload.hive.blog that was causing authentication errors --- .gitlab-ci.yml | 45 +++++++++------------------------------------ 1 file changed, 9 insertions(+), 36 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a4273fd..dc19b34 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -52,25 +52,6 @@ build_images: . docker push "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_SHORT_SHA" - # Push only the main image to registry.hive.blog (if credentials are available) - echo "Current branch: $CI_COMMIT_BRANCH" - echo "Is protected: $CI_COMMIT_REF_PROTECTED" - echo "Pipeline source: $CI_PIPELINE_SOURCE" - - if [[ -n "$BLOG_REGISTRY_USER" ]] && [[ -n "$BLOG_REGISTRY_PASSWORD" ]]; then - echo "Pushing to registry.hive.blog..." - docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_SHORT_SHA" "registry.hive.blog/nft_tracker:$CI_COMMIT_SHORT_SHA" - docker tag "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_SHORT_SHA" "registry.hive.blog/nft_tracker/postgrest-rewriter:$CI_COMMIT_SHORT_SHA" - echo "Logging in to registry-upload.hive.blog as user: $BLOG_REGISTRY_USER" - echo "$BLOG_REGISTRY_PASSWORD" | docker login --username "$BLOG_REGISTRY_USER" --password-stdin registry-upload.hive.blog - docker push "registry.hive.blog/nft_tracker:$CI_COMMIT_SHORT_SHA" - docker push "registry.hive.blog/nft_tracker/postgrest-rewriter:$CI_COMMIT_SHORT_SHA" - else - echo "WARNING: BLOG_REGISTRY_USER or BLOG_REGISTRY_PASSWORD not set." - echo "Skipping push to registry.hive.blog (this is expected for non-protected branches)" - echo "Images are still available in GitLab registry at: registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_SHORT_SHA" - fi - # If on develop branch, also tag and push as 'develop' if [[ "$CI_COMMIT_BRANCH" == "develop" ]]; then echo "Tagging images with 'develop' tag..." @@ -78,14 +59,6 @@ build_images: docker tag "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_SHORT_SHA" "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:develop" docker push "registry.gitlab.syncad.com/hive/nft_tracker:develop" docker push "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:develop" - - # Only push develop tag to registry.hive.blog if we have credentials - if [[ -n "$BLOG_REGISTRY_USER" ]] && [[ -n "$BLOG_REGISTRY_PASSWORD" ]]; then - docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_SHORT_SHA" "registry.hive.blog/nft_tracker:develop" - docker tag "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_SHORT_SHA" "registry.hive.blog/nft_tracker/postgrest-rewriter:develop" - docker push "registry.hive.blog/nft_tracker:develop" - docker push "registry.hive.blog/nft_tracker/postgrest-rewriter:develop" - fi fi tags: - public-runner-docker @@ -124,21 +97,21 @@ publish_release_images: . docker push "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_TAG" - # Push only the main images to registry.hive.blog - docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_TAG" "registry.hive.blog/nft_tracker:$CI_COMMIT_TAG" - docker tag "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_TAG" "registry.hive.blog/nft_tracker/postgrest-rewriter:$CI_COMMIT_TAG" - docker push "registry.hive.blog/nft_tracker:$CI_COMMIT_TAG" - docker push "registry.hive.blog/nft_tracker/postgrest-rewriter:$CI_COMMIT_TAG" + # Push only the main images to registry-upload.hive.blog + docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_TAG" "registry-upload.hive.blog/nft_tracker:$CI_COMMIT_TAG" + docker tag "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_TAG" "registry-upload.hive.blog/nft_tracker/postgrest-rewriter:$CI_COMMIT_TAG" + docker push "registry-upload.hive.blog/nft_tracker:$CI_COMMIT_TAG" + docker push "registry-upload.hive.blog/nft_tracker/postgrest-rewriter:$CI_COMMIT_TAG" # Also tag with 'latest' if this is a release tag docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_TAG" "registry.gitlab.syncad.com/hive/nft_tracker:latest" docker tag "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_TAG" "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:latest" - docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_TAG" "registry.hive.blog/nft_tracker:latest" - docker tag "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_TAG" "registry.hive.blog/nft_tracker/postgrest-rewriter:latest" + docker tag "registry.gitlab.syncad.com/hive/nft_tracker:$CI_COMMIT_TAG" "registry-upload.hive.blog/nft_tracker:latest" + docker tag "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:$CI_COMMIT_TAG" "registry-upload.hive.blog/nft_tracker/postgrest-rewriter:latest" docker push "registry.gitlab.syncad.com/hive/nft_tracker:latest" docker push "registry.gitlab.syncad.com/hive/nft_tracker/postgrest-rewriter:latest" - docker push "registry.hive.blog/nft_tracker:latest" - docker push "registry.hive.blog/nft_tracker/postgrest-rewriter:latest" + docker push "registry-upload.hive.blog/nft_tracker:latest" + docker push "registry-upload.hive.blog/nft_tracker/postgrest-rewriter:latest" echo "Successfully published release images with tag: $CI_COMMIT_TAG" rules: -- GitLab From 18ee8d3e502601ec2e3720814edfa8347e22f0e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Le=C5=9Bniak?= Date: Mon, 1 Sep 2025 12:54:04 +0200 Subject: [PATCH 25/35] Support for multi ids actions --- db/nft_actions.sql | 77 ++++++++++++++----- tests/prelude.sql | 30 ++++---- .../test_multi_unsupported_operation.out | 2 +- tests/regression/expected/test_set_data.out | 4 +- .../expected/test_set_data_disallowed.out | 6 +- tests/regression/expected/test_soulbind.out | 4 +- .../expected/test_soulbind_disallowed.out | 6 +- .../expected/test_soulbind_namespacing.out | 2 +- tests/regression/expected/test_transfer.out | 6 +- .../expected/test_transfer_burn.out | 14 ++-- .../expected/test_transfer_multi.out | 6 +- .../expected/test_transfer_soulbound.out | 6 +- .../sql/test_multi_unsupported_operation.sql | 2 +- tests/regression/sql/test_set_data.sql | 4 +- .../sql/test_set_data_disallowed.sql | 6 +- tests/regression/sql/test_soulbind.sql | 4 +- .../sql/test_soulbind_disallowed.sql | 6 +- .../sql/test_soulbind_namespacing.sql | 2 +- tests/regression/sql/test_transfer.sql | 4 +- tests/regression/sql/test_transfer_burn.sql | 10 +-- tests/regression/sql/test_transfer_multi.sql | 4 +- .../sql/test_transfer_soulbound.sql | 6 +- 22 files changed, 126 insertions(+), 85 deletions(-) diff --git a/db/nft_actions.sql b/db/nft_actions.sql index 8c8f7db..0aa0e4f 100644 --- a/db/nft_actions.sql +++ b/db/nft_actions.sql @@ -51,10 +51,10 @@ BEGIN END; $$; --- Returns true if given account is NFT instance holder. +-- Returns true if given account is holder for all given instances. CREATE OR REPLACE FUNCTION nfttracker_app.is_holder( IN _symbol nfttracker_app.symbol, - IN _id INT, + IN _ids INT[], IN _account hive.account_name_type ) RETURNS bool @@ -63,18 +63,42 @@ STABLE AS $$ DECLARE - _holder BOOLEAN; + _count INT; BEGIN - SELECT COUNT(*) > 0 INTO _holder + SELECT COUNT(*) INTO _count FROM nfttracker_app.instances AS i JOIN nfttracker_app.types AS t ON t.id = i.type_id JOIN hafd.accounts AS a ON a.name = _account JOIN hafd.accounts AS ns ON ns.name = _symbol.namespace WHERE t.symbol = _symbol.name AND t.creator = ns.id - AND i.id = _id + AND i.id = ANY(_ids) AND i.holder = a.id; - RETURN _holder; + RETURN _count = ARRAY_LENGTH(_ids, 1); +END; +$$; + +-- Returns true if all given instances exist. +CREATE OR REPLACE FUNCTION nfttracker_app.instances_exist( + IN _symbol nfttracker_app.symbol, + IN _ids INT[] +) +RETURNS bool +LANGUAGE plpgsql +STABLE +AS +$$ +DECLARE + _count INT; +BEGIN + SELECT COUNT(*) INTO _count + FROM nfttracker_app.instances AS i + JOIN nfttracker_app.types AS t ON t.id = i.type_id + JOIN hafd.accounts AS ns ON ns.name = _symbol.namespace + WHERE t.symbol = _symbol.name + AND t.creator = ns.id + AND i.id = ANY(_ids); + RETURN _count = ARRAY_LENGTH(_ids, 1); END; $$; @@ -389,19 +413,26 @@ RETURNS VOID LANGUAGE 'plpgsql' VOLATILE AS $$ +DECLARE + _symbol nfttracker_app.symbol := (_json->>'symbol')::nfttracker_app.symbol; + _ids INT[]; BEGIN - IF NOT nfttracker_app.is_authorized(_json->>'symbol', _account) THEN + SELECT ARRAY(SELECT jsonb_array_elements_text(_json->'ids')::INT) INTO _ids; + IF NOT nfttracker_app.instances_exist(_symbol, _ids) THEN + RAISE EXCEPTION 'NFTs %:% do not exist', _symbol, _ids; + END IF; + IF NOT nfttracker_app.is_authorized(_symbol, _account) THEN RAISE EXCEPTION 'Account % is disallowed to soulbind NFTs %', _account, _json->>'symbol'; END IF; UPDATE nfttracker_app.instances AS i SET soulbound = j.soulbound, updated_at = b.created_at - FROM jsonb_to_record(_json) AS j(symbol text, id INT, soulbound boolean) + FROM jsonb_to_record(_json) AS j(symbol text, ids INT[], soulbound boolean) JOIN hafd.accounts AS ns ON ns.name = (j.symbol::nfttracker_app.symbol).namespace JOIN nfttracker_app.types AS t ON t.symbol = (j.symbol::nfttracker_app.symbol).name AND t.creator = ns.id JOIN hafd.blocks AS b ON b.num = _block_num - WHERE i.id = j.id AND i.type_id = t.id; + WHERE i.id = ANY(j.ids) AND i.type_id = t.id; END $$; @@ -414,18 +445,25 @@ RETURNS VOID LANGUAGE 'plpgsql' VOLATILE AS $$ +DECLARE + _symbol nfttracker_app.symbol := (_json->>'symbol')::nfttracker_app.symbol; + _ids INT[]; BEGIN - IF NOT nfttracker_app.is_authorized(_json->>'symbol', _account) THEN + SELECT ARRAY(SELECT jsonb_array_elements_text(_json->'ids')::INT) INTO _ids; + IF NOT nfttracker_app.instances_exist(_symbol, _ids) THEN + RAISE EXCEPTION 'NFTs %:% do not exist', _symbol, _ids; + END IF; + IF NOT nfttracker_app.is_authorized(_symbol, _account) THEN RAISE EXCEPTION 'Account % is disallowed to set data on NFTs %', _account, _json->>'symbol'; END IF; UPDATE nfttracker_app.instances AS i SET data = j.data, updated_at = b.created_at - FROM jsonb_to_record(_json) AS j(symbol text, id INT, data jsonb) + FROM jsonb_to_record(_json) AS j(symbol text, ids INT[], data jsonb) JOIN nfttracker_app.types AS t ON t.symbol = (j.symbol::nfttracker_app.symbol).name JOIN hafd.blocks AS b ON b.num = _block_num - WHERE i.id = j.id AND i.type_id = t.id; + WHERE i.id = ANY(j.ids) AND i.type_id = t.id; END $$; @@ -440,22 +478,25 @@ VOLATILE AS $$ DECLARE _symbol nfttracker_app.symbol; - _id INT; + _ids INT[]; BEGIN _symbol := (_json->>'symbol')::nfttracker_app.symbol; - _id := (_json->>'id')::INT; - IF NOT nfttracker_app.is_holder(_symbol, _id, _account) THEN - RAISE EXCEPTION 'Account % is disallowed to transfer NFT %:%', _account, _json->>'symbol', _id; + SELECT ARRAY(SELECT jsonb_array_elements_text(_json->'ids')::INT) INTO _ids; + IF NOT nfttracker_app.instances_exist(_symbol, _ids) THEN + RAISE EXCEPTION 'NFTs %:% do not exist', _json->>'symbol', _ids; + END IF; + IF NOT nfttracker_app.is_holder(_symbol, _ids, _account) THEN + RAISE EXCEPTION 'Account % is disallowed to transfer NFTs %:%', _account, _json->>'symbol', _ids; END IF; UPDATE nfttracker_app.instances AS i SET holder = a.id, updated_at = b.created_at - FROM jsonb_to_record(_json) AS j(symbol text, id INT, "to" hive.account_name_type) + FROM jsonb_to_record(_json) AS j(symbol text, ids INT[], "to" hive.account_name_type) JOIN nfttracker_app.types AS t ON t.symbol = (j.symbol::nfttracker_app.symbol).name JOIN hafd.blocks AS b ON b.num = _block_num JOIN hafd.accounts AS a ON a.name = j."to" - WHERE i.id = j.id AND i.type_id = t.id AND (NOT i.soulbound OR j."to" = 'null'); + WHERE i.id = ANY(j.ids) AND i.type_id = t.id AND (NOT i.soulbound OR j."to" = 'null'); END $$; diff --git a/tests/prelude.sql b/tests/prelude.sql index 3023f7c..a98c030 100644 --- a/tests/prelude.sql +++ b/tests/prelude.sql @@ -93,66 +93,66 @@ BEGIN END; $$; -CREATE OR REPLACE FUNCTION nft_soulbind_op(symbol TEXT, id INT, soulbound bool) +CREATE OR REPLACE FUNCTION nft_soulbind_op(symbol TEXT, ids INT[], soulbound bool) RETURNS TEXT LANGUAGE plpgsql AS $$ BEGIN - RETURN format('{"action": "soulbind", "symbol": %s, "id": %s, "soulbound": %s}', + RETURN format('{"action": "soulbind", "symbol": %s, "ids": %s, "soulbound": %s}', to_jsonb(symbol)::text, - to_jsonb(id)::text, + to_jsonb(ids)::text, to_jsonb(soulbound)::text ); END; $$; -CREATE OR REPLACE PROCEDURE insert_nft_soulbind_op(block_num INT, auth hive.account_name_type, symbol TEXT, id INT, soulbound bool, pos INT DEFAULT 0) +CREATE OR REPLACE PROCEDURE insert_nft_soulbind_op(block_num INT, auth hive.account_name_type, symbol TEXT, ids INT[], soulbound bool, pos INT DEFAULT 0) LANGUAGE plpgsql AS $$ BEGIN - CALL insert_nft_operation(block_num, pos, auth, nft_soulbind_op(symbol, id, soulbound)::jsonb); + CALL insert_nft_operation(block_num, pos, auth, nft_soulbind_op(symbol, ids, soulbound)::jsonb); END; $$; -CREATE OR REPLACE FUNCTION nft_set_data_op(symbol TEXT, id INT, data jsonb) +CREATE OR REPLACE FUNCTION nft_set_data_op(symbol TEXT, ids INT[], data jsonb) RETURNS TEXT LANGUAGE plpgsql AS $$ BEGIN - RETURN format('{"action": "set_data", "symbol": %s, "id": %s, "data": %s}', + RETURN format('{"action": "set_data", "symbol": %s, "ids": %s, "data": %s}', to_jsonb(symbol)::text, - to_jsonb(id)::text, + to_jsonb(ids)::text, to_jsonb(data)::text ); END; $$; -CREATE OR REPLACE PROCEDURE insert_nft_set_data_op(block_num INT, auth hive.account_name_type, symbol TEXT, id INT, data jsonb, pos INT DEFAULT 0) +CREATE OR REPLACE PROCEDURE insert_nft_set_data_op(block_num INT, auth hive.account_name_type, symbol TEXT, ids INT[], data jsonb, pos INT DEFAULT 0) LANGUAGE plpgsql AS $$ BEGIN - CALL insert_nft_operation(block_num, pos, auth, nft_set_data_op(symbol, id, data)::jsonb); + CALL insert_nft_operation(block_num, pos, auth, nft_set_data_op(symbol, ids, data)::jsonb); END; $$; -CREATE OR REPLACE FUNCTION nft_transfer_op(symbol TEXT, id INT, to_account hive.account_name_type) +CREATE OR REPLACE FUNCTION nft_transfer_op(symbol TEXT, ids INT[], to_account hive.account_name_type) RETURNS TEXT LANGUAGE plpgsql AS $$ BEGIN - RETURN format('{"action": "transfer", "symbol": %s, "id": %s, "to": %s}', + RETURN format('{"action": "transfer", "symbol": %s, "ids": %s, "to": %s}', to_jsonb(symbol)::text, - to_jsonb(id)::text, + to_jsonb(ids)::text, to_jsonb(to_account)::text ); END; $$; -CREATE OR REPLACE PROCEDURE insert_nft_transfer_op(block_num INT, auth hive.account_name_type, symbol TEXT, id INT, to_account hive.account_name_type, pos INT DEFAULT 0) +CREATE OR REPLACE PROCEDURE insert_nft_transfer_op(block_num INT, auth hive.account_name_type, symbol TEXT, ids INT[], to_account hive.account_name_type, pos INT DEFAULT 0) LANGUAGE plpgsql AS $$ BEGIN - CALL insert_nft_operation(block_num, pos, auth, nft_transfer_op(symbol, id, to_account)::jsonb); + CALL insert_nft_operation(block_num, pos, auth, nft_transfer_op(symbol, ids, to_account)::jsonb); END; $$; diff --git a/tests/regression/expected/test_multi_unsupported_operation.out b/tests/regression/expected/test_multi_unsupported_operation.out index 5955af4..ad6d44c 100644 --- a/tests/regression/expected/test_multi_unsupported_operation.out +++ b/tests/regression/expected/test_multi_unsupported_operation.out @@ -4,7 +4,7 @@ CALL insert_nft_ops(block_num=>1, auth=>'alice', ops=>ARRAY[ nft_register_op(symbol=>'alice/XYZ', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12), nft_issue_op(symbol=>'alice/XYZ', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE), '{"action": "nonexistent_action"}', - nft_transfer_op(symbol=>'alice/XYZ', id=>1, to_account=>'charlie') + nft_transfer_op(symbol=>'alice/XYZ', ids=>ARRAY[1], to_account=>'charlie') ]); -- When CALL nfttracker_sync_blocks(); diff --git a/tests/regression/expected/test_set_data.out b/tests/regression/expected/test_set_data.out index 08ad918..1b7570b 100644 --- a/tests/regression/expected/test_set_data.out +++ b/tests/regression/expected/test_set_data.out @@ -2,8 +2,8 @@ -- Given CALL insert_nft_register_op(block_num=>1, auth=>'alice', symbol=>'alice/XYZ', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); CALL insert_nft_issue_op(block_num=>2, auth=>'alice', symbol=>'alice/XYZ', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); -CALL insert_nft_set_data_op(block_num=>3, auth=>'alice', symbol=>'alice/XYZ', id=>1, data=>'{"key1": "value1"}'::jsonb); -CALL insert_nft_set_data_op(block_num=>4, auth=>'alice', symbol=>'alice/XYZ', id=>1, data=>'{"key2": "value2"}'::jsonb); +CALL insert_nft_set_data_op(block_num=>3, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[1], data=>'{"key1": "value1"}'::jsonb); +CALL insert_nft_set_data_op(block_num=>4, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[1], data=>'{"key2": "value2"}'::jsonb); -- When CALL nfttracker_sync_blocks(); NOTICE: Last block processed by application: 0 diff --git a/tests/regression/expected/test_set_data_disallowed.out b/tests/regression/expected/test_set_data_disallowed.out index 6023bf7..505f97e 100644 --- a/tests/regression/expected/test_set_data_disallowed.out +++ b/tests/regression/expected/test_set_data_disallowed.out @@ -3,9 +3,9 @@ CALL insert_nft_register_op(block_num=>1, auth=>'alice', symbol=>'alice/XYZ', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); CALL insert_nft_issue_op(block_num=>2, auth=>'alice', symbol=>'alice/XYZ', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); CALL insert_nft_modify_op(block_num=>3, auth=>'alice', symbol=>'alice/XYZ', name=>'test', owner=>'alice', issuers=>ARRAY[]::hive.account_name_type[], max_count=>12); -CALL insert_nft_set_data_op(block_num=>4, pos=>1, auth=>'alice', symbol=>'alice/XYZ', id=>1, data=>'{"by": "alice"}'::jsonb); -CALL insert_nft_set_data_op(block_num=>4, pos=>2, auth=>'bob', symbol=>'alice/XYZ', id=>1, data=>'{"by": "bob"}'::jsonb); -CALL insert_nft_set_data_op(block_num=>4, pos=>3, auth=>'charlie', symbol=>'alice/XYZ', id=>1, data=>'{"by": "charlie"}'::jsonb); +CALL insert_nft_set_data_op(block_num=>4, pos=>1, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[1], data=>'{"by": "alice"}'::jsonb); +CALL insert_nft_set_data_op(block_num=>4, pos=>2, auth=>'bob', symbol=>'alice/XYZ', ids=>ARRAY[1], data=>'{"by": "bob"}'::jsonb); +CALL insert_nft_set_data_op(block_num=>4, pos=>3, auth=>'charlie', symbol=>'alice/XYZ', ids=>ARRAY[1], data=>'{"by": "charlie"}'::jsonb); -- When CALL nfttracker_sync_blocks(); NOTICE: Last block processed by application: 0 diff --git a/tests/regression/expected/test_soulbind.out b/tests/regression/expected/test_soulbind.out index 0781d7a..0c93ee9 100644 --- a/tests/regression/expected/test_soulbind.out +++ b/tests/regression/expected/test_soulbind.out @@ -2,8 +2,8 @@ -- Given CALL insert_nft_register_op(block_num=>1, auth=>'alice', symbol=>'alice/ABC', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); CALL insert_nft_issue_op(block_num=>2, auth=>'alice', symbol=>'alice/ABC', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); -CALL insert_nft_soulbind_op(block_num=>3, auth=>'alice', symbol=>'alice/ABC', id=>1, soulbound=>TRUE); -CALL insert_nft_soulbind_op(block_num=>4, auth=>'alice', symbol=>'alice/ABC', id=>1, soulbound=>TRUE); +CALL insert_nft_soulbind_op(block_num=>3, auth=>'alice', symbol=>'alice/ABC', ids=>ARRAY[1], soulbound=>TRUE); +CALL insert_nft_soulbind_op(block_num=>4, auth=>'alice', symbol=>'alice/ABC', ids=>ARRAY[1], soulbound=>TRUE); -- When CALL nfttracker_sync_blocks(); NOTICE: Last block processed by application: 0 diff --git a/tests/regression/expected/test_soulbind_disallowed.out b/tests/regression/expected/test_soulbind_disallowed.out index 0fc248a..c6cf26a 100644 --- a/tests/regression/expected/test_soulbind_disallowed.out +++ b/tests/regression/expected/test_soulbind_disallowed.out @@ -3,9 +3,9 @@ CALL insert_nft_register_op(block_num=>1, auth=>'alice', symbol=>'alice/ABC', name=>'test', owner=>'alice', issuers=>ARRAY['alice']::hive.account_name_type[], max_count=>12); CALL insert_nft_issue_op(block_num=>2, auth=>'alice', symbol=>'alice/ABC', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); CALL insert_nft_modify_op(block_num=>3, auth=>'alice', symbol=>'alice/ABC', name=>'test', owner=>'alice', issuers=>ARRAY[]::hive.account_name_type[], max_count=>12); -CALL insert_nft_soulbind_op(block_num=>4, auth=>'alice', symbol=>'alice/ABC', id=>1, soulbound=>TRUE); -CALL insert_nft_soulbind_op(block_num=>5, auth=>'bob', symbol=>'alice/ABC', id=>1, soulbound=>TRUE); -CALL insert_nft_soulbind_op(block_num=>6, auth=>'charlie', symbol=>'alice/ABC', id=>1, soulbound=>TRUE); +CALL insert_nft_soulbind_op(block_num=>4, auth=>'alice', symbol=>'alice/ABC', ids=>ARRAY[1], soulbound=>TRUE); +CALL insert_nft_soulbind_op(block_num=>5, auth=>'bob', symbol=>'alice/ABC', ids=>ARRAY[1], soulbound=>TRUE); +CALL insert_nft_soulbind_op(block_num=>6, auth=>'charlie', symbol=>'alice/ABC', ids=>ARRAY[1], soulbound=>TRUE); -- When CALL nfttracker_sync_blocks(); NOTICE: Last block processed by application: 0 diff --git a/tests/regression/expected/test_soulbind_namespacing.out b/tests/regression/expected/test_soulbind_namespacing.out index 780d950..4a0968c 100644 --- a/tests/regression/expected/test_soulbind_namespacing.out +++ b/tests/regression/expected/test_soulbind_namespacing.out @@ -6,7 +6,7 @@ CALL insert_nft_register_op(block_num=>1, pos=>3, auth=>'charlie', symbol=>'char CALL insert_nft_issue_op(block_num=>2, pos=>1, auth=>'alice', symbol=>'alice/ABC', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); CALL insert_nft_issue_op(block_num=>2, pos=>2, auth=>'bob', symbol=>'bob/ABC', holder=>'bob', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); CALL insert_nft_issue_op(block_num=>2, pos=>3, auth=>'charlie', symbol=>'charlie/ABC', holder=>'charlie', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); -CALL insert_nft_soulbind_op(block_num=>3, pos=>1, auth=>'alice', symbol=>'alice/ABC', id=>1, soulbound=>TRUE); +CALL insert_nft_soulbind_op(block_num=>3, pos=>1, auth=>'alice', symbol=>'alice/ABC', ids=>ARRAY[1], soulbound=>TRUE); -- When CALL nfttracker_sync_blocks(); NOTICE: Last block processed by application: 0 diff --git a/tests/regression/expected/test_transfer.out b/tests/regression/expected/test_transfer.out index 5a85088..d99228b 100644 --- a/tests/regression/expected/test_transfer.out +++ b/tests/regression/expected/test_transfer.out @@ -2,8 +2,8 @@ -- Given CALL insert_nft_register_op(block_num=>1, auth=>'alice', symbol=>'alice/XYZ', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); CALL insert_nft_issue_op(block_num=>2, auth=>'alice', symbol=>'alice/XYZ', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); -CALL insert_nft_transfer_op(block_num=>3, auth=>'alice', symbol=>'alice/XYZ', id=>1, to_account=>'bob'); -CALL insert_nft_transfer_op(block_num=>4, auth=>'alice', symbol=>'alice/XYZ', id=>1, to_account=>'charlie'); +CALL insert_nft_transfer_op(block_num=>3, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[1], to_account=>'bob'); +CALL insert_nft_transfer_op(block_num=>4, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[1], to_account=>'charlie'); -- When CALL nfttracker_sync_blocks(); NOTICE: Last block processed by application: 0 @@ -23,7 +23,7 @@ NOTICE: nfttracker processing block: 3... NOTICE: nfttracker processed block 3 successfully in _ s NOTICE: nfttracker processing block: 4... -WARNING: Error processing action transfer in block 4: Account alice is disallowed to transfer NFT alice/XYZ:1 +WARNING: Error processing action transfer in block 4: Account alice is disallowed to transfer NFTs alice/XYZ:{1} NOTICE: nfttracker processed block 4 successfully in _ s NOTICE: Blocks limit reached. Exiting application main loop at processed block: 4. diff --git a/tests/regression/expected/test_transfer_burn.out b/tests/regression/expected/test_transfer_burn.out index 0947b5d..20832e7 100644 --- a/tests/regression/expected/test_transfer_burn.out +++ b/tests/regression/expected/test_transfer_burn.out @@ -3,11 +3,11 @@ CALL insert_nft_register_op(block_num=>1, auth=>'alice', symbol=>'alice/XYZ', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); CALL insert_nft_issue_op(block_num=>2, pos=>1, auth=>'alice', symbol=>'alice/XYZ', holder=>'alice', data=>'{"bound": false}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); CALL insert_nft_issue_op(block_num=>2, pos=>2, auth=>'alice', symbol=>'alice/XYZ', holder=>'alice', data=>'{"bound": true}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>TRUE); -CALL insert_nft_soulbind_op(block_num=>3, auth=>'alice', symbol=>'alice/XYZ', id=>1, soulbound=>TRUE); -CALL insert_nft_transfer_op(block_num=>4, pos=>1, auth=>'alice', symbol=>'alice/XYZ', id=>1, to_account=>'null'); -CALL insert_nft_transfer_op(block_num=>4, pos=>2, auth=>'alice', symbol=>'alice/XYZ', id=>2, to_account=>'null'); -CALL insert_nft_transfer_op(block_num=>4, pos=>3, auth=>'alice', symbol=>'alice/XYZ', id=>1, to_account=>'alice'); -CALL insert_nft_transfer_op(block_num=>4, pos=>4, auth=>'alice', symbol=>'alice/XYZ', id=>2, to_account=>'alice'); +CALL insert_nft_soulbind_op(block_num=>3, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[1], soulbound=>TRUE); +CALL insert_nft_transfer_op(block_num=>4, pos=>1, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[1], to_account=>'null'); +CALL insert_nft_transfer_op(block_num=>4, pos=>2, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[2], to_account=>'null'); +CALL insert_nft_transfer_op(block_num=>4, pos=>3, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[1], to_account=>'alice'); +CALL insert_nft_transfer_op(block_num=>4, pos=>4, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[2], to_account=>'alice'); -- When CALL nfttracker_sync_blocks(); NOTICE: Last block processed by application: 0 @@ -27,8 +27,8 @@ NOTICE: nfttracker processing block: 3... NOTICE: nfttracker processed block 3 successfully in _ s NOTICE: nfttracker processing block: 4... -WARNING: Error processing action transfer in block 4: Account alice is disallowed to transfer NFT alice/XYZ:1 -WARNING: Error processing action transfer in block 4: Account alice is disallowed to transfer NFT alice/XYZ:2 +WARNING: Error processing action transfer in block 4: Account alice is disallowed to transfer NFTs alice/XYZ:{1} +WARNING: Error processing action transfer in block 4: Account alice is disallowed to transfer NFTs alice/XYZ:{2} NOTICE: nfttracker processed block 4 successfully in _ s NOTICE: Blocks limit reached. Exiting application main loop at processed block: 4. diff --git a/tests/regression/expected/test_transfer_multi.out b/tests/regression/expected/test_transfer_multi.out index bc6fefa..00488d1 100644 --- a/tests/regression/expected/test_transfer_multi.out +++ b/tests/regression/expected/test_transfer_multi.out @@ -3,8 +3,8 @@ CALL insert_nft_ops(block_num=>1, auth=>'alice', ops=>ARRAY[ nft_register_op(symbol=>'alice/XYZ', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12), nft_issue_op(symbol=>'alice/XYZ', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE), - nft_transfer_op(symbol=>'alice/XYZ', id=>1, to_account=>'bob'), - nft_transfer_op(symbol=>'alice/XYZ', id=>1, to_account=>'charlie') + nft_transfer_op(symbol=>'alice/XYZ', ids=>ARRAY[1], to_account=>'bob'), + nft_transfer_op(symbol=>'alice/XYZ', ids=>ARRAY[1], to_account=>'charlie') ]); -- When CALL nfttracker_sync_blocks(); @@ -16,7 +16,7 @@ WARNING: Waiting for next block... NOTICE: HAF instance is ready. Exiting wait loop. WARNING: PROFILE: 'nfttracker_app' STAGE_CHANGED from 'N/A' to 'live' after N/A block: 1 fork: 1 head block: 9 head fork: 1 NOTICE: nfttracker processing block: 1... -WARNING: Error processing action transfer in block 1: Account alice is disallowed to transfer NFT alice/XYZ:1 +WARNING: Error processing action transfer in block 1: Account alice is disallowed to transfer NFTs alice/XYZ:{1} NOTICE: nfttracker processed block 1 successfully in _ s NOTICE: Blocks limit reached. Exiting application main loop at processed block: 1. diff --git a/tests/regression/expected/test_transfer_soulbound.out b/tests/regression/expected/test_transfer_soulbound.out index 5b838b0..ffa7a77 100644 --- a/tests/regression/expected/test_transfer_soulbound.out +++ b/tests/regression/expected/test_transfer_soulbound.out @@ -3,12 +3,12 @@ -- unsoulbound NFT that's later soulbound CALL insert_nft_register_op(block_num=>1, pos=>0, auth=>'alice', symbol=>'alice/AAA', name=>'foo', owner=>'alice', issuers=>ARRAY['alice'], max_count=>10); CALL insert_nft_issue_op(block_num=>2, pos=>0, auth=>'alice', symbol=>'alice/AAA', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); -CALL insert_nft_soulbind_op(block_num=>3, pos=>0, auth=>'alice', symbol=>'alice/AAA', id=>1, soulbound=>TRUE); -CALL insert_nft_transfer_op(block_num=>4, pos=>0, auth=>'alice', symbol=>'alice/AAA', id=>1, to_account=>'dan'); +CALL insert_nft_soulbind_op(block_num=>3, pos=>0, auth=>'alice', symbol=>'alice/AAA', ids=>ARRAY[1], soulbound=>TRUE); +CALL insert_nft_transfer_op(block_num=>4, pos=>0, auth=>'alice', symbol=>'alice/AAA', ids=>ARRAY[1], to_account=>'dan'); -- NFT soulbound on creation CALL insert_nft_register_op(block_num=>1, pos=>1, auth=>'bob', symbol=>'bob/BBB', name=>'bar', owner=>'bob', issuers=>ARRAY['bob'], max_count=>10); CALL insert_nft_issue_op(block_num=>2, pos=>1, auth=>'bob', symbol=>'bob/BBB', holder=>'bob', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>TRUE); -CALL insert_nft_transfer_op(block_num=>3, pos=>1, auth=>'bob', symbol=>'bob/BBB', id=>2, to_account=>'dan'); +CALL insert_nft_transfer_op(block_num=>3, pos=>1, auth=>'bob', symbol=>'bob/BBB', ids=>ARRAY[2], to_account=>'dan'); -- When CALL nfttracker_sync_blocks(); NOTICE: Last block processed by application: 0 diff --git a/tests/regression/sql/test_multi_unsupported_operation.sql b/tests/regression/sql/test_multi_unsupported_operation.sql index 7bb7741..e1de2f8 100644 --- a/tests/regression/sql/test_multi_unsupported_operation.sql +++ b/tests/regression/sql/test_multi_unsupported_operation.sql @@ -5,7 +5,7 @@ CALL insert_nft_ops(block_num=>1, auth=>'alice', ops=>ARRAY[ nft_register_op(symbol=>'alice/XYZ', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12), nft_issue_op(symbol=>'alice/XYZ', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE), '{"action": "nonexistent_action"}', - nft_transfer_op(symbol=>'alice/XYZ', id=>1, to_account=>'charlie') + nft_transfer_op(symbol=>'alice/XYZ', ids=>ARRAY[1], to_account=>'charlie') ]); -- When diff --git a/tests/regression/sql/test_set_data.sql b/tests/regression/sql/test_set_data.sql index a7beecc..e56121a 100644 --- a/tests/regression/sql/test_set_data.sql +++ b/tests/regression/sql/test_set_data.sql @@ -3,8 +3,8 @@ -- Given CALL insert_nft_register_op(block_num=>1, auth=>'alice', symbol=>'alice/XYZ', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); CALL insert_nft_issue_op(block_num=>2, auth=>'alice', symbol=>'alice/XYZ', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); -CALL insert_nft_set_data_op(block_num=>3, auth=>'alice', symbol=>'alice/XYZ', id=>1, data=>'{"key1": "value1"}'::jsonb); -CALL insert_nft_set_data_op(block_num=>4, auth=>'alice', symbol=>'alice/XYZ', id=>1, data=>'{"key2": "value2"}'::jsonb); +CALL insert_nft_set_data_op(block_num=>3, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[1], data=>'{"key1": "value1"}'::jsonb); +CALL insert_nft_set_data_op(block_num=>4, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[1], data=>'{"key2": "value2"}'::jsonb); -- When CALL nfttracker_sync_blocks(); diff --git a/tests/regression/sql/test_set_data_disallowed.sql b/tests/regression/sql/test_set_data_disallowed.sql index a0ba185..90e9aad 100644 --- a/tests/regression/sql/test_set_data_disallowed.sql +++ b/tests/regression/sql/test_set_data_disallowed.sql @@ -4,9 +4,9 @@ CALL insert_nft_register_op(block_num=>1, auth=>'alice', symbol=>'alice/XYZ', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); CALL insert_nft_issue_op(block_num=>2, auth=>'alice', symbol=>'alice/XYZ', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); CALL insert_nft_modify_op(block_num=>3, auth=>'alice', symbol=>'alice/XYZ', name=>'test', owner=>'alice', issuers=>ARRAY[]::hive.account_name_type[], max_count=>12); -CALL insert_nft_set_data_op(block_num=>4, pos=>1, auth=>'alice', symbol=>'alice/XYZ', id=>1, data=>'{"by": "alice"}'::jsonb); -CALL insert_nft_set_data_op(block_num=>4, pos=>2, auth=>'bob', symbol=>'alice/XYZ', id=>1, data=>'{"by": "bob"}'::jsonb); -CALL insert_nft_set_data_op(block_num=>4, pos=>3, auth=>'charlie', symbol=>'alice/XYZ', id=>1, data=>'{"by": "charlie"}'::jsonb); +CALL insert_nft_set_data_op(block_num=>4, pos=>1, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[1], data=>'{"by": "alice"}'::jsonb); +CALL insert_nft_set_data_op(block_num=>4, pos=>2, auth=>'bob', symbol=>'alice/XYZ', ids=>ARRAY[1], data=>'{"by": "bob"}'::jsonb); +CALL insert_nft_set_data_op(block_num=>4, pos=>3, auth=>'charlie', symbol=>'alice/XYZ', ids=>ARRAY[1], data=>'{"by": "charlie"}'::jsonb); -- When CALL nfttracker_sync_blocks(); diff --git a/tests/regression/sql/test_soulbind.sql b/tests/regression/sql/test_soulbind.sql index cd875c0..4687667 100644 --- a/tests/regression/sql/test_soulbind.sql +++ b/tests/regression/sql/test_soulbind.sql @@ -3,8 +3,8 @@ -- Given CALL insert_nft_register_op(block_num=>1, auth=>'alice', symbol=>'alice/ABC', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); CALL insert_nft_issue_op(block_num=>2, auth=>'alice', symbol=>'alice/ABC', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); -CALL insert_nft_soulbind_op(block_num=>3, auth=>'alice', symbol=>'alice/ABC', id=>1, soulbound=>TRUE); -CALL insert_nft_soulbind_op(block_num=>4, auth=>'alice', symbol=>'alice/ABC', id=>1, soulbound=>TRUE); +CALL insert_nft_soulbind_op(block_num=>3, auth=>'alice', symbol=>'alice/ABC', ids=>ARRAY[1], soulbound=>TRUE); +CALL insert_nft_soulbind_op(block_num=>4, auth=>'alice', symbol=>'alice/ABC', ids=>ARRAY[1], soulbound=>TRUE); -- When CALL nfttracker_sync_blocks(); diff --git a/tests/regression/sql/test_soulbind_disallowed.sql b/tests/regression/sql/test_soulbind_disallowed.sql index 631a73a..9ce52dd 100644 --- a/tests/regression/sql/test_soulbind_disallowed.sql +++ b/tests/regression/sql/test_soulbind_disallowed.sql @@ -4,9 +4,9 @@ CALL insert_nft_register_op(block_num=>1, auth=>'alice', symbol=>'alice/ABC', name=>'test', owner=>'alice', issuers=>ARRAY['alice']::hive.account_name_type[], max_count=>12); CALL insert_nft_issue_op(block_num=>2, auth=>'alice', symbol=>'alice/ABC', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); CALL insert_nft_modify_op(block_num=>3, auth=>'alice', symbol=>'alice/ABC', name=>'test', owner=>'alice', issuers=>ARRAY[]::hive.account_name_type[], max_count=>12); -CALL insert_nft_soulbind_op(block_num=>4, auth=>'alice', symbol=>'alice/ABC', id=>1, soulbound=>TRUE); -CALL insert_nft_soulbind_op(block_num=>5, auth=>'bob', symbol=>'alice/ABC', id=>1, soulbound=>TRUE); -CALL insert_nft_soulbind_op(block_num=>6, auth=>'charlie', symbol=>'alice/ABC', id=>1, soulbound=>TRUE); +CALL insert_nft_soulbind_op(block_num=>4, auth=>'alice', symbol=>'alice/ABC', ids=>ARRAY[1], soulbound=>TRUE); +CALL insert_nft_soulbind_op(block_num=>5, auth=>'bob', symbol=>'alice/ABC', ids=>ARRAY[1], soulbound=>TRUE); +CALL insert_nft_soulbind_op(block_num=>6, auth=>'charlie', symbol=>'alice/ABC', ids=>ARRAY[1], soulbound=>TRUE); -- When CALL nfttracker_sync_blocks(); diff --git a/tests/regression/sql/test_soulbind_namespacing.sql b/tests/regression/sql/test_soulbind_namespacing.sql index 0a5f11a..22f7ec1 100644 --- a/tests/regression/sql/test_soulbind_namespacing.sql +++ b/tests/regression/sql/test_soulbind_namespacing.sql @@ -7,7 +7,7 @@ CALL insert_nft_register_op(block_num=>1, pos=>3, auth=>'charlie', symbol=>'char CALL insert_nft_issue_op(block_num=>2, pos=>1, auth=>'alice', symbol=>'alice/ABC', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); CALL insert_nft_issue_op(block_num=>2, pos=>2, auth=>'bob', symbol=>'bob/ABC', holder=>'bob', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); CALL insert_nft_issue_op(block_num=>2, pos=>3, auth=>'charlie', symbol=>'charlie/ABC', holder=>'charlie', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); -CALL insert_nft_soulbind_op(block_num=>3, pos=>1, auth=>'alice', symbol=>'alice/ABC', id=>1, soulbound=>TRUE); +CALL insert_nft_soulbind_op(block_num=>3, pos=>1, auth=>'alice', symbol=>'alice/ABC', ids=>ARRAY[1], soulbound=>TRUE); -- When CALL nfttracker_sync_blocks(); diff --git a/tests/regression/sql/test_transfer.sql b/tests/regression/sql/test_transfer.sql index d860ac6..4b6677d 100644 --- a/tests/regression/sql/test_transfer.sql +++ b/tests/regression/sql/test_transfer.sql @@ -3,8 +3,8 @@ -- Given CALL insert_nft_register_op(block_num=>1, auth=>'alice', symbol=>'alice/XYZ', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); CALL insert_nft_issue_op(block_num=>2, auth=>'alice', symbol=>'alice/XYZ', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); -CALL insert_nft_transfer_op(block_num=>3, auth=>'alice', symbol=>'alice/XYZ', id=>1, to_account=>'bob'); -CALL insert_nft_transfer_op(block_num=>4, auth=>'alice', symbol=>'alice/XYZ', id=>1, to_account=>'charlie'); +CALL insert_nft_transfer_op(block_num=>3, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[1], to_account=>'bob'); +CALL insert_nft_transfer_op(block_num=>4, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[1], to_account=>'charlie'); -- When CALL nfttracker_sync_blocks(); diff --git a/tests/regression/sql/test_transfer_burn.sql b/tests/regression/sql/test_transfer_burn.sql index 6d6bdfd..cb07423 100644 --- a/tests/regression/sql/test_transfer_burn.sql +++ b/tests/regression/sql/test_transfer_burn.sql @@ -4,11 +4,11 @@ CALL insert_nft_register_op(block_num=>1, auth=>'alice', symbol=>'alice/XYZ', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); CALL insert_nft_issue_op(block_num=>2, pos=>1, auth=>'alice', symbol=>'alice/XYZ', holder=>'alice', data=>'{"bound": false}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); CALL insert_nft_issue_op(block_num=>2, pos=>2, auth=>'alice', symbol=>'alice/XYZ', holder=>'alice', data=>'{"bound": true}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>TRUE); -CALL insert_nft_soulbind_op(block_num=>3, auth=>'alice', symbol=>'alice/XYZ', id=>1, soulbound=>TRUE); -CALL insert_nft_transfer_op(block_num=>4, pos=>1, auth=>'alice', symbol=>'alice/XYZ', id=>1, to_account=>'null'); -CALL insert_nft_transfer_op(block_num=>4, pos=>2, auth=>'alice', symbol=>'alice/XYZ', id=>2, to_account=>'null'); -CALL insert_nft_transfer_op(block_num=>4, pos=>3, auth=>'alice', symbol=>'alice/XYZ', id=>1, to_account=>'alice'); -CALL insert_nft_transfer_op(block_num=>4, pos=>4, auth=>'alice', symbol=>'alice/XYZ', id=>2, to_account=>'alice'); +CALL insert_nft_soulbind_op(block_num=>3, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[1], soulbound=>TRUE); +CALL insert_nft_transfer_op(block_num=>4, pos=>1, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[1], to_account=>'null'); +CALL insert_nft_transfer_op(block_num=>4, pos=>2, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[2], to_account=>'null'); +CALL insert_nft_transfer_op(block_num=>4, pos=>3, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[1], to_account=>'alice'); +CALL insert_nft_transfer_op(block_num=>4, pos=>4, auth=>'alice', symbol=>'alice/XYZ', ids=>ARRAY[2], to_account=>'alice'); -- When CALL nfttracker_sync_blocks(); diff --git a/tests/regression/sql/test_transfer_multi.sql b/tests/regression/sql/test_transfer_multi.sql index dcb0e6f..977650c 100644 --- a/tests/regression/sql/test_transfer_multi.sql +++ b/tests/regression/sql/test_transfer_multi.sql @@ -4,8 +4,8 @@ CALL insert_nft_ops(block_num=>1, auth=>'alice', ops=>ARRAY[ nft_register_op(symbol=>'alice/XYZ', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12), nft_issue_op(symbol=>'alice/XYZ', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE), - nft_transfer_op(symbol=>'alice/XYZ', id=>1, to_account=>'bob'), - nft_transfer_op(symbol=>'alice/XYZ', id=>1, to_account=>'charlie') + nft_transfer_op(symbol=>'alice/XYZ', ids=>ARRAY[1], to_account=>'bob'), + nft_transfer_op(symbol=>'alice/XYZ', ids=>ARRAY[1], to_account=>'charlie') ]); -- When diff --git a/tests/regression/sql/test_transfer_soulbound.sql b/tests/regression/sql/test_transfer_soulbound.sql index d0b9456..2eb5cee 100644 --- a/tests/regression/sql/test_transfer_soulbound.sql +++ b/tests/regression/sql/test_transfer_soulbound.sql @@ -4,12 +4,12 @@ -- unsoulbound NFT that's later soulbound CALL insert_nft_register_op(block_num=>1, pos=>0, auth=>'alice', symbol=>'alice/AAA', name=>'foo', owner=>'alice', issuers=>ARRAY['alice'], max_count=>10); CALL insert_nft_issue_op(block_num=>2, pos=>0, auth=>'alice', symbol=>'alice/AAA', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); -CALL insert_nft_soulbind_op(block_num=>3, pos=>0, auth=>'alice', symbol=>'alice/AAA', id=>1, soulbound=>TRUE); -CALL insert_nft_transfer_op(block_num=>4, pos=>0, auth=>'alice', symbol=>'alice/AAA', id=>1, to_account=>'dan'); +CALL insert_nft_soulbind_op(block_num=>3, pos=>0, auth=>'alice', symbol=>'alice/AAA', ids=>ARRAY[1], soulbound=>TRUE); +CALL insert_nft_transfer_op(block_num=>4, pos=>0, auth=>'alice', symbol=>'alice/AAA', ids=>ARRAY[1], to_account=>'dan'); -- NFT soulbound on creation CALL insert_nft_register_op(block_num=>1, pos=>1, auth=>'bob', symbol=>'bob/BBB', name=>'bar', owner=>'bob', issuers=>ARRAY['bob'], max_count=>10); CALL insert_nft_issue_op(block_num=>2, pos=>1, auth=>'bob', symbol=>'bob/BBB', holder=>'bob', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>TRUE); -CALL insert_nft_transfer_op(block_num=>3, pos=>1, auth=>'bob', symbol=>'bob/BBB', id=>2, to_account=>'dan'); +CALL insert_nft_transfer_op(block_num=>3, pos=>1, auth=>'bob', symbol=>'bob/BBB', ids=>ARRAY[2], to_account=>'dan'); -- When CALL nfttracker_sync_blocks(); -- GitLab From 395a52c81863dbbda8419d89b29968d4a5943bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Le=C5=9Bniak?= Date: Mon, 1 Sep 2025 17:23:16 +0200 Subject: [PATCH 26/35] Extra tests for multi ids actions --- .../expected/test_set_data_multi_ids.out | 59 +++++++++++++++++++ .../expected/test_soulbind_multi_ids.out | 59 +++++++++++++++++++ .../expected/test_transfer_multi_ids.out | 59 +++++++++++++++++++ .../sql/test_set_data_multi_ids.sql | 22 +++++++ .../sql/test_soulbind_multi_ids.sql | 22 +++++++ .../sql/test_transfer_multi_ids.sql | 22 +++++++ 6 files changed, 243 insertions(+) create mode 100644 tests/regression/expected/test_set_data_multi_ids.out create mode 100644 tests/regression/expected/test_soulbind_multi_ids.out create mode 100644 tests/regression/expected/test_transfer_multi_ids.out create mode 100644 tests/regression/sql/test_set_data_multi_ids.sql create mode 100644 tests/regression/sql/test_soulbind_multi_ids.sql create mode 100644 tests/regression/sql/test_transfer_multi_ids.sql diff --git a/tests/regression/expected/test_set_data_multi_ids.out b/tests/regression/expected/test_set_data_multi_ids.out new file mode 100644 index 0000000..dfe7cd8 --- /dev/null +++ b/tests/regression/expected/test_set_data_multi_ids.out @@ -0,0 +1,59 @@ +-- Check setting custom data on multiple instances +-- Given +CALL insert_nft_register_op(block_num=>1, pos=>1, auth=>'alice', symbol=>'alice/X', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); +CALL insert_nft_register_op(block_num=>1, pos=>2, auth=>'alice', symbol=>'alice/Y', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); +CALL insert_nft_register_op(block_num=>1, pos=>3, auth=>'alice', symbol=>'alice/Z', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); +CALL insert_nft_issue_op(block_num=>2, pos=>1, auth=>'alice', symbol=>'alice/X', holder=>'alice', data=>'{"x":1}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>2, auth=>'alice', symbol=>'alice/X', holder=>'alice', data=>'{"x":2}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>3, auth=>'alice', symbol=>'alice/Z', holder=>'alice', data=>'{"_":0}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>4, auth=>'alice', symbol=>'alice/Y', holder=>'alice', data=>'{"y":1}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>5, auth=>'alice', symbol=>'alice/Y', holder=>'alice', data=>'{"y":2}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_set_data_op(block_num=>3, pos=>1, auth=>'alice', symbol=>'alice/X', ids=>ARRAY[1,2], data=>'{"x": true}'::jsonb); +CALL insert_nft_set_data_op(block_num=>3, pos=>2, auth=>'alice', symbol=>'alice/Z', ids=>ARRAY[3,0], data=>'{"z": true}'::jsonb); +CALL insert_nft_set_data_op(block_num=>3, pos=>3, auth=>'alice', symbol=>'alice/Y', ids=>ARRAY[4,5], data=>'{"y": true}'::jsonb); +-- When +CALL nfttracker_sync_blocks(); +NOTICE: Last block processed by application: 0 +NOTICE: Entering application main loop... +WARNING: PROFILE: 'nfttracker_app' ATTACHED stage: 'N/A' block: 0 fork: 1 head block: 9 head fork: 1 +NOTICE: HAF instance is ready. Exiting wait loop. +WARNING: Waiting for next block... +NOTICE: HAF instance is ready. Exiting wait loop. +WARNING: PROFILE: 'nfttracker_app' STAGE_CHANGED from 'N/A' to 'live' after N/A block: 1 fork: 1 head block: 9 head fork: 1 +NOTICE: nfttracker processing block: 1... +NOTICE: nfttracker processed block 1 successfully in _ s + +NOTICE: nfttracker processing block: 2... +NOTICE: nfttracker processed block 2 successfully in _ s + +NOTICE: nfttracker processing block: 3... +WARNING: Error processing action set_data in block 3: NFTs (alice,Z):{3,0} do not exist +NOTICE: nfttracker processed block 3 successfully in _ s + +NOTICE: Blocks limit reached. Exiting application main loop at processed block: 3. +-- Then +SELECT creator, owner, symbol::TEXT, holder, data, tags, soulbound FROM instances_view; + creator | owner | symbol | holder | data | tags | soulbound +---------+-------+--------+--------+-------------+------+----------- + alice | alice | X | alice | {"x": true} | {} | f + alice | alice | X | alice | {"x": true} | {} | f + alice | alice | Z | alice | {"_": 0} | {} | f + alice | alice | Y | alice | {"y": true} | {} | f + alice | alice | Y | alice | {"y": true} | {} | f +(5 rows) + +SELECT updated_at > created_at FROM instances_view WHERE id<>3; + ?column? +---------- + t + t + t + t +(4 rows) + +SELECT updated_at = created_at FROM instances_view WHERE id=3; + ?column? +---------- + t +(1 row) + diff --git a/tests/regression/expected/test_soulbind_multi_ids.out b/tests/regression/expected/test_soulbind_multi_ids.out new file mode 100644 index 0000000..e32fa6d --- /dev/null +++ b/tests/regression/expected/test_soulbind_multi_ids.out @@ -0,0 +1,59 @@ +-- Check soulbinding multiple instances +-- Given +CALL insert_nft_register_op(block_num=>1, pos=>1, auth=>'alice', symbol=>'alice/A', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); +CALL insert_nft_register_op(block_num=>1, pos=>2, auth=>'alice', symbol=>'alice/B', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); +CALL insert_nft_register_op(block_num=>1, pos=>3, auth=>'alice', symbol=>'alice/Z', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); +CALL insert_nft_issue_op(block_num=>2, pos=>1, auth=>'alice', symbol=>'alice/A', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>2, auth=>'alice', symbol=>'alice/A', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>3, auth=>'alice', symbol=>'alice/Z', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>4, auth=>'alice', symbol=>'alice/B', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>5, auth=>'alice', symbol=>'alice/B', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_soulbind_op(block_num=>3, pos=>1, auth=>'alice', symbol=>'alice/A', ids=>ARRAY[1, 2], soulbound=>TRUE); +CALL insert_nft_soulbind_op(block_num=>3, pos=>2, auth=>'alice', symbol=>'alice/Z', ids=>ARRAY[3, 0], soulbound=>TRUE); +CALL insert_nft_soulbind_op(block_num=>3, pos=>3, auth=>'alice', symbol=>'alice/B', ids=>ARRAY[4, 5], soulbound=>TRUE); +-- When +CALL nfttracker_sync_blocks(); +NOTICE: Last block processed by application: 0 +NOTICE: Entering application main loop... +WARNING: PROFILE: 'nfttracker_app' ATTACHED stage: 'N/A' block: 0 fork: 1 head block: 9 head fork: 1 +NOTICE: HAF instance is ready. Exiting wait loop. +WARNING: Waiting for next block... +NOTICE: HAF instance is ready. Exiting wait loop. +WARNING: PROFILE: 'nfttracker_app' STAGE_CHANGED from 'N/A' to 'live' after N/A block: 1 fork: 1 head block: 9 head fork: 1 +NOTICE: nfttracker processing block: 1... +NOTICE: nfttracker processed block 1 successfully in _ s + +NOTICE: nfttracker processing block: 2... +NOTICE: nfttracker processed block 2 successfully in _ s + +NOTICE: nfttracker processing block: 3... +WARNING: Error processing action soulbind in block 3: NFTs (alice,Z):{3,0} do not exist +NOTICE: nfttracker processed block 3 successfully in _ s + +NOTICE: Blocks limit reached. Exiting application main loop at processed block: 3. +-- Then +SELECT creator, owner, symbol::TEXT, holder, data, tags, soulbound FROM instances_view; + creator | owner | symbol | holder | data | tags | soulbound +---------+-------+--------+--------+------+------+----------- + alice | alice | A | alice | {} | {} | t + alice | alice | A | alice | {} | {} | t + alice | alice | Z | alice | {} | {} | f + alice | alice | B | alice | {} | {} | t + alice | alice | B | alice | {} | {} | t +(5 rows) + +SELECT updated_at > created_at FROM instances_view WHERE id<>3; + ?column? +---------- + t + t + t + t +(4 rows) + +SELECT updated_at = created_at FROM instances_view WHERE id=3; + ?column? +---------- + t +(1 row) + diff --git a/tests/regression/expected/test_transfer_multi_ids.out b/tests/regression/expected/test_transfer_multi_ids.out new file mode 100644 index 0000000..4139a30 --- /dev/null +++ b/tests/regression/expected/test_transfer_multi_ids.out @@ -0,0 +1,59 @@ +-- Check transferring multiple instances +-- Given +CALL insert_nft_register_op(block_num=>1, pos=>1, auth=>'alice', symbol=>'alice/X', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); +CALL insert_nft_register_op(block_num=>1, pos=>2, auth=>'alice', symbol=>'alice/Y', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); +CALL insert_nft_register_op(block_num=>1, pos=>3, auth=>'alice', symbol=>'alice/Z', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); +CALL insert_nft_issue_op(block_num=>2, pos=>1, auth=>'alice', symbol=>'alice/X', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>2, auth=>'alice', symbol=>'alice/X', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>3, auth=>'alice', symbol=>'alice/Z', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>4, auth=>'alice', symbol=>'alice/Y', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>5, auth=>'alice', symbol=>'alice/Y', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_transfer_op(block_num=>3, pos=>1, auth=>'alice', symbol=>'alice/X', ids=>ARRAY[1, 2], to_account=>'bob'); +CALL insert_nft_transfer_op(block_num=>3, pos=>2, auth=>'alice', symbol=>'alice/Z', ids=>ARRAY[3, 0], to_account=>'dan'); +CALL insert_nft_transfer_op(block_num=>3, pos=>3, auth=>'alice', symbol=>'alice/Y', ids=>ARRAY[4, 5], to_account=>'charlie'); +-- When +CALL nfttracker_sync_blocks(); +NOTICE: Last block processed by application: 0 +NOTICE: Entering application main loop... +WARNING: PROFILE: 'nfttracker_app' ATTACHED stage: 'N/A' block: 0 fork: 1 head block: 9 head fork: 1 +NOTICE: HAF instance is ready. Exiting wait loop. +WARNING: Waiting for next block... +NOTICE: HAF instance is ready. Exiting wait loop. +WARNING: PROFILE: 'nfttracker_app' STAGE_CHANGED from 'N/A' to 'live' after N/A block: 1 fork: 1 head block: 9 head fork: 1 +NOTICE: nfttracker processing block: 1... +NOTICE: nfttracker processed block 1 successfully in _ s + +NOTICE: nfttracker processing block: 2... +NOTICE: nfttracker processed block 2 successfully in _ s + +NOTICE: nfttracker processing block: 3... +WARNING: Error processing action transfer in block 3: NFTs alice/Z:{3,0} do not exist +NOTICE: nfttracker processed block 3 successfully in _ s + +NOTICE: Blocks limit reached. Exiting application main loop at processed block: 3. +-- Then +SELECT creator, owner, symbol, holder, data, tags, soulbound FROM instances_view; + creator | owner | symbol | holder | data | tags | soulbound +---------+-------+--------+---------+------+------+----------- + alice | alice | X | bob | {} | {} | f + alice | alice | X | bob | {} | {} | f + alice | alice | Z | alice | {} | {} | f + alice | alice | Y | charlie | {} | {} | f + alice | alice | Y | charlie | {} | {} | f +(5 rows) + +SELECT updated_at > created_at FROM instances_view WHERE id<>3; + ?column? +---------- + t + t + t + t +(4 rows) + +SELECT updated_at = created_at FROM instances_view WHERE id=3; + ?column? +---------- + t +(1 row) + diff --git a/tests/regression/sql/test_set_data_multi_ids.sql b/tests/regression/sql/test_set_data_multi_ids.sql new file mode 100644 index 0000000..19e370b --- /dev/null +++ b/tests/regression/sql/test_set_data_multi_ids.sql @@ -0,0 +1,22 @@ +-- Check setting custom data on multiple instances + +-- Given +CALL insert_nft_register_op(block_num=>1, pos=>1, auth=>'alice', symbol=>'alice/X', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); +CALL insert_nft_register_op(block_num=>1, pos=>2, auth=>'alice', symbol=>'alice/Y', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); +CALL insert_nft_register_op(block_num=>1, pos=>3, auth=>'alice', symbol=>'alice/Z', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); +CALL insert_nft_issue_op(block_num=>2, pos=>1, auth=>'alice', symbol=>'alice/X', holder=>'alice', data=>'{"x":1}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>2, auth=>'alice', symbol=>'alice/X', holder=>'alice', data=>'{"x":2}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>3, auth=>'alice', symbol=>'alice/Z', holder=>'alice', data=>'{"_":0}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>4, auth=>'alice', symbol=>'alice/Y', holder=>'alice', data=>'{"y":1}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>5, auth=>'alice', symbol=>'alice/Y', holder=>'alice', data=>'{"y":2}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_set_data_op(block_num=>3, pos=>1, auth=>'alice', symbol=>'alice/X', ids=>ARRAY[1,2], data=>'{"x": true}'::jsonb); +CALL insert_nft_set_data_op(block_num=>3, pos=>2, auth=>'alice', symbol=>'alice/Z', ids=>ARRAY[3,0], data=>'{"z": true}'::jsonb); +CALL insert_nft_set_data_op(block_num=>3, pos=>3, auth=>'alice', symbol=>'alice/Y', ids=>ARRAY[4,5], data=>'{"y": true}'::jsonb); + +-- When +CALL nfttracker_sync_blocks(); + +-- Then +SELECT creator, owner, symbol::TEXT, holder, data, tags, soulbound FROM instances_view; +SELECT updated_at > created_at FROM instances_view WHERE id<>3; +SELECT updated_at = created_at FROM instances_view WHERE id=3; diff --git a/tests/regression/sql/test_soulbind_multi_ids.sql b/tests/regression/sql/test_soulbind_multi_ids.sql new file mode 100644 index 0000000..4fa97f1 --- /dev/null +++ b/tests/regression/sql/test_soulbind_multi_ids.sql @@ -0,0 +1,22 @@ +-- Check soulbinding multiple instances + +-- Given +CALL insert_nft_register_op(block_num=>1, pos=>1, auth=>'alice', symbol=>'alice/A', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); +CALL insert_nft_register_op(block_num=>1, pos=>2, auth=>'alice', symbol=>'alice/B', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); +CALL insert_nft_register_op(block_num=>1, pos=>3, auth=>'alice', symbol=>'alice/Z', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); +CALL insert_nft_issue_op(block_num=>2, pos=>1, auth=>'alice', symbol=>'alice/A', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>2, auth=>'alice', symbol=>'alice/A', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>3, auth=>'alice', symbol=>'alice/Z', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>4, auth=>'alice', symbol=>'alice/B', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>5, auth=>'alice', symbol=>'alice/B', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_soulbind_op(block_num=>3, pos=>1, auth=>'alice', symbol=>'alice/A', ids=>ARRAY[1, 2], soulbound=>TRUE); +CALL insert_nft_soulbind_op(block_num=>3, pos=>2, auth=>'alice', symbol=>'alice/Z', ids=>ARRAY[3, 0], soulbound=>TRUE); +CALL insert_nft_soulbind_op(block_num=>3, pos=>3, auth=>'alice', symbol=>'alice/B', ids=>ARRAY[4, 5], soulbound=>TRUE); + +-- When +CALL nfttracker_sync_blocks(); + +-- Then +SELECT creator, owner, symbol::TEXT, holder, data, tags, soulbound FROM instances_view; +SELECT updated_at > created_at FROM instances_view WHERE id<>3; +SELECT updated_at = created_at FROM instances_view WHERE id=3; diff --git a/tests/regression/sql/test_transfer_multi_ids.sql b/tests/regression/sql/test_transfer_multi_ids.sql new file mode 100644 index 0000000..c4ab9be --- /dev/null +++ b/tests/regression/sql/test_transfer_multi_ids.sql @@ -0,0 +1,22 @@ +-- Check transferring multiple instances + +-- Given +CALL insert_nft_register_op(block_num=>1, pos=>1, auth=>'alice', symbol=>'alice/X', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); +CALL insert_nft_register_op(block_num=>1, pos=>2, auth=>'alice', symbol=>'alice/Y', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); +CALL insert_nft_register_op(block_num=>1, pos=>3, auth=>'alice', symbol=>'alice/Z', name=>'test', owner=>'alice', issuers=>ARRAY['alice'], max_count=>12); +CALL insert_nft_issue_op(block_num=>2, pos=>1, auth=>'alice', symbol=>'alice/X', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>2, auth=>'alice', symbol=>'alice/X', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>3, auth=>'alice', symbol=>'alice/Z', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>4, auth=>'alice', symbol=>'alice/Y', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_issue_op(block_num=>2, pos=>5, auth=>'alice', symbol=>'alice/Y', holder=>'alice', data=>'{}', tags=>ARRAY[]::nfttracker_app.tags, soulbound=>FALSE); +CALL insert_nft_transfer_op(block_num=>3, pos=>1, auth=>'alice', symbol=>'alice/X', ids=>ARRAY[1, 2], to_account=>'bob'); +CALL insert_nft_transfer_op(block_num=>3, pos=>2, auth=>'alice', symbol=>'alice/Z', ids=>ARRAY[3, 0], to_account=>'dan'); +CALL insert_nft_transfer_op(block_num=>3, pos=>3, auth=>'alice', symbol=>'alice/Y', ids=>ARRAY[4, 5], to_account=>'charlie'); + +-- When +CALL nfttracker_sync_blocks(); + +-- Then +SELECT creator, owner, symbol, holder, data, tags, soulbound FROM instances_view; +SELECT updated_at > created_at FROM instances_view WHERE id<>3; +SELECT updated_at = created_at FROM instances_view WHERE id=3; -- GitLab From ca302c9cda6355898073de59a09850b0ae70e2e6 Mon Sep 17 00:00:00 2001 From: Dan Notestein Date: Tue, 16 Sep 2025 19:19:49 +0000 Subject: [PATCH 27/35] Update submodules: - haf: develop (a2031fd663ef74d12e20d6aac5537ce20eca992f) --- haf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/haf b/haf index b077291..a2031fd 160000 --- a/haf +++ b/haf @@ -1 +1 @@ -Subproject commit b077291d37d71d482fd22fa68ab23c905a5c0912 +Subproject commit a2031fd663ef74d12e20d6aac5537ce20eca992f -- GitLab From b02d2a2f8544d98cc0e2e96dead0889afea07e76 Mon Sep 17 00:00:00 2001 From: Dan Notestein Date: Tue, 16 Sep 2025 21:47:57 +0000 Subject: [PATCH 28/35] Update submodules: - haf: update-submodules-for-test-for-rc2 (d4bca1524076323418802ea33995d8f7948b929c) --- haf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/haf b/haf index a2031fd..d4bca15 160000 --- a/haf +++ b/haf @@ -1 +1 @@ -Subproject commit a2031fd663ef74d12e20d6aac5537ce20eca992f +Subproject commit d4bca1524076323418802ea33995d8f7948b929c -- GitLab From e1688452cf81c56d7401306d0a27cf87512270d3 Mon Sep 17 00:00:00 2001 From: Dan Notestein Date: Wed, 17 Sep 2025 20:31:12 +0000 Subject: [PATCH 29/35] Update submodules: - haf: develop (80e558bdeec9556ec46927853cf390c72ef03311) --- haf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/haf b/haf index d4bca15..80e558b 160000 --- a/haf +++ b/haf @@ -1 +1 @@ -Subproject commit d4bca1524076323418802ea33995d8f7948b929c +Subproject commit 80e558bdeec9556ec46927853cf390c72ef03311 -- GitLab From cabc2d6812660b52d4fcd44482bc5536a4cb1413 Mon Sep 17 00:00:00 2001 From: Dan Notestein Date: Thu, 2 Oct 2025 00:44:38 +0000 Subject: [PATCH 30/35] Update submodules: - haf: update-submodules-for-1.27.12-rc3 (9d922f221b84981996238ccb2bdb6325ad3271ab) --- haf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/haf b/haf index 80e558b..9d922f2 160000 --- a/haf +++ b/haf @@ -1 +1 @@ -Subproject commit 80e558bdeec9556ec46927853cf390c72ef03311 +Subproject commit 9d922f221b84981996238ccb2bdb6325ad3271ab -- GitLab From cf7fe327b1a5d8e02a355f2c8829a564ab2da02e Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Thu, 2 Oct 2025 11:32:44 -0400 Subject: [PATCH 31/35] Fix block processing healthcheck to not report unhealthy during initial sync --- .../scripts/block-processing-healthcheck.sh | 29 +++++++++++++++++-- scripts/process_blocks.sh | 1 + 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/docker/scripts/block-processing-healthcheck.sh b/docker/scripts/block-processing-healthcheck.sh index f623df5..520a43f 100755 --- a/docker/scripts/block-processing-healthcheck.sh +++ b/docker/scripts/block-processing-healthcheck.sh @@ -8,6 +8,31 @@ trap 'trap - 2 15 && kill -- -$$' 2 15 postgres_user=${POSTGRES_USER:-"haf_admin"} postgres_host=${POSTGRES_HOST:-"localhost"} postgres_port=${POSTGRES_PORT:-5432} -POSTGRES_ACCESS=${POSTGRES_URL:-"postgresql://$postgres_user@$postgres_host:$postgres_port/haf_block_log"} +POSTGRES_ACCESS=${POSTGRES_URL:-"postgresql://$postgres_user@$postgres_host:$postgres_port/haf_block_log?application_name=nft_tracker_health_check"} -exec [ "$(psql "$POSTGRES_ACCESS" --quiet --no-align --tuples-only --command="SELECT hive.is_app_in_sync('nfttracker_app');")" = t ] +# this health check will return healthy if: +# - nft_tracker has processed a block in the last 60 seconds +# (as long as it was also after the container started, we don't want +# to report healthy immediately after a restart) +# or +# - nft_tracker's head block has caught up to haf's irreversible block +# (so we don't mark nft_tracker as unhealthy if HAF stops getting blocks) +# +# This check needs to know when the block processing started, so the docker entrypoint +# must write this to a file like: +# date -u +"%Y-%m-%dT%H:%M:%S+00:00" > /tmp/block_processing_startup_time.txt +if [ ! -f "/tmp/block_processing_startup_time.txt" ]; then + echo "file /tmp/block_processing_startup_time.txt does not exist, which means block" + echo "processing hasn't started yet" + exit 1 +fi +STARTUP_TIME="$(cat /tmp/block_processing_startup_time.txt)" +CHECK="SET TIME ZONE 'UTC'; \ + SELECT ((now() - (SELECT last_active_at FROM hafd.contexts WHERE name = 'nfttracker_app') < interval '1 minute') \ + AND (SELECT last_active_at FROM hafd.contexts WHERE name = 'nfttracker_app') > '${STARTUP_TIME}'::timestamp) OR \ + hive.is_app_in_sync('nfttracker_app');" + + +# the docker container probably won't have a locale set, do this to suppress the warning +export LC_ALL=C +exec [ "$(psql "$POSTGRES_ACCESS" --quiet --no-align --tuples-only --command="${CHECK}")" = t ] diff --git a/scripts/process_blocks.sh b/scripts/process_blocks.sh index 8570093..f8e47d5 100755 --- a/scripts/process_blocks.sh +++ b/scripts/process_blocks.sh @@ -68,6 +68,7 @@ POSTGRES_ACCESS=${POSTGRES_URL:-"postgresql://$POSTGRES_USER@$POSTGRES_HOST:$POS process_blocks() { local n_blocks="${1:-null}" log_file="nfttracker_sync.log" + date -u +"%Y-%m-%dT%H:%M:%S+00:00" > /tmp/block_processing_startup_time.txt psql "$POSTGRES_ACCESS" -v "ON_ERROR_STOP=on" -v NFTTRACKER_SCHEMA="${NFTTRACKER_SCHEMA}" -c "\timing" -c "CALL ${NFTTRACKER_SCHEMA}.main('${NFTTRACKER_SCHEMA}', $n_blocks);" 2>&1 | tee -i "$log_file" } -- GitLab From 7583405e7f342bbdbfc52990b200d94e452dc539 Mon Sep 17 00:00:00 2001 From: Dan Notestein Date: Mon, 6 Oct 2025 23:32:58 +0000 Subject: [PATCH 32/35] Update submodules: - haf: update-submodules-for-1.27.12-rc4 (4c5cc6d9645e19fbbc135cefc6da942fdf13de73) --- haf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/haf b/haf index 9d922f2..4c5cc6d 160000 --- a/haf +++ b/haf @@ -1 +1 @@ -Subproject commit 9d922f221b84981996238ccb2bdb6325ad3271ab +Subproject commit 4c5cc6d9645e19fbbc135cefc6da942fdf13de73 -- GitLab From df540cd855fbed068a7559a46b4147f580b612b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Le=C5=9Bniak?= Date: Mon, 20 Oct 2025 16:19:32 +0200 Subject: [PATCH 33/35] Fix url in examples nfts-api -> nft-tracker-api --- endpoints/endpoint_schema.sql | 6 +++--- endpoints/get_nft_instances.sql | 2 +- endpoints/get_nft_instances_with_tags.sql | 2 +- endpoints/get_nft_types.sql | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/endpoints/endpoint_schema.sql b/endpoints/endpoint_schema.sql index 7cfc395..1e63d84 100644 --- a/endpoints/endpoint_schema.sql +++ b/endpoints/endpoint_schema.sql @@ -193,7 +193,7 @@ DO $__$ "NFT" ], "summary": "NFT types", - "description": "Returns registered NFT types.\n\nSQL example\n* `SELECT * FROM nfttracker_endpoints.get_nft_types();`\n\nREST call example\n* `GET ''https://%1$s/nfts-api/nfts''`\n", + "description": "Returns registered NFT types.\n\nSQL example\n* `SELECT * FROM nfttracker_endpoints.get_nft_types();`\n\nREST call example\n* `GET ''https://%1$s/nft-tracker-api/nfts''`\n", "operationId": "nfttracker_endpoints.get_nft_types", "responses": { "200": { @@ -230,7 +230,7 @@ DO $__$ "NFT" ], "summary": "NFT instances", - "description": "Returns issued instances of given NFT symbol.\n\nSQL example\n* `SELECT * FROM nfttracker_endpoints.get_nft_instances(''alice'', ''TEST'');`\n\nREST call example\n* `GET ''https://%1$s/nfts-api/nfts/alice/TEST''`\n", + "description": "Returns issued instances of given NFT symbol.\n\nSQL example\n* `SELECT * FROM nfttracker_endpoints.get_nft_instances(''alice'', ''TEST'');`\n\nREST call example\n* `GET ''https://%1$s/nft-tracker-api/nfts/alice/TEST''`\n", "operationId": "nfttracker_endpoints.get_nft_instances", "parameters": [ { @@ -292,7 +292,7 @@ DO $__$ "NFT" ], "summary": "NFT instances", - "description": "Returns issued instances of given NFT symbol.\n\nSQL example\n* `SELECT * FROM nfttracker_endpoints.get_nft_instances(''alice'', ''TEST'');`\n\nREST call example\n* `GET ''https://%1$s/nfts-api/nfts/alice/TEST''`\n", + "description": "Returns issued instances of given NFT symbol.\n\nSQL example\n* `SELECT * FROM nfttracker_endpoints.get_nft_instances(''alice'', ''TEST'');`\n\nREST call example\n* `GET ''https://%1$s/nft-tracker-api/nfts/alice/TEST''`\n", "operationId": "nfttracker_endpoints.get_nft_instances_with_tags", "parameters": [ { diff --git a/endpoints/get_nft_instances.sql b/endpoints/get_nft_instances.sql index f4139d3..56a7cf2 100644 --- a/endpoints/get_nft_instances.sql +++ b/endpoints/get_nft_instances.sql @@ -13,7 +13,7 @@ SET ROLE nfttracker_owner; * `SELECT * FROM nfttracker_endpoints.get_nft_instances(''alice'', ''TEST'');` REST call example - * `GET ''https://%1$s/nfts-api/nfts/alice/TEST''` + * `GET ''https://%1$s/nft-tracker-api/nfts/alice/TEST''` operationId: nfttracker_endpoints.get_nft_instances parameters: - in: path diff --git a/endpoints/get_nft_instances_with_tags.sql b/endpoints/get_nft_instances_with_tags.sql index 6b7c5ab..e38056b 100644 --- a/endpoints/get_nft_instances_with_tags.sql +++ b/endpoints/get_nft_instances_with_tags.sql @@ -13,7 +13,7 @@ SET ROLE nfttracker_owner; * `SELECT * FROM nfttracker_endpoints.get_nft_instances(''alice'', ''TEST'');` REST call example - * `GET ''https://%1$s/nfts-api/nfts/alice/TEST''` + * `GET ''https://%1$s/nft-tracker-api/nfts/alice/TEST''` operationId: nfttracker_endpoints.get_nft_instances_with_tags parameters: - in: path diff --git a/endpoints/get_nft_types.sql b/endpoints/get_nft_types.sql index 8798f8e..b2b0707 100644 --- a/endpoints/get_nft_types.sql +++ b/endpoints/get_nft_types.sql @@ -13,7 +13,7 @@ SET ROLE nfttracker_owner; * `SELECT * FROM nfttracker_endpoints.get_nft_types();` REST call example - * `GET ''https://%1$s/nfts-api/nfts''` + * `GET ''https://%1$s/nft-tracker-api/nfts''` operationId: nfttracker_endpoints.get_nft_types responses: '200': -- GitLab From d606d2832b724a9b50cd2af801d24acbc087a8ee Mon Sep 17 00:00:00 2001 From: Dan Notestein Date: Wed, 29 Oct 2025 20:16:25 +0000 Subject: [PATCH 34/35] Update submodules: - haf: develop (5e15be4e8351e096ee436dcc8294ea4c7bed4bac) --- haf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/haf b/haf index 4c5cc6d..5e15be4 160000 --- a/haf +++ b/haf @@ -1 +1 @@ -Subproject commit 4c5cc6d9645e19fbbc135cefc6da942fdf13de73 +Subproject commit 5e15be4e8351e096ee436dcc8294ea4c7bed4bac -- GitLab From ea00d8894c0c07d761e1467160c349fc99ba0a6c Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Wed, 5 Nov 2025 16:11:59 -0500 Subject: [PATCH 35/35] Update submodules: - haf: release/1.28.3 (a4daf3db6b8cfa6e0bf5a9bffd1550db3f65c118) --- haf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/haf b/haf index 5e15be4..a4daf3d 160000 --- a/haf +++ b/haf @@ -1 +1 @@ -Subproject commit 5e15be4e8351e096ee436dcc8294ea4c7bed4bac +Subproject commit a4daf3db6b8cfa6e0bf5a9bffd1550db3f65c118 -- GitLab