Skip to content

Commit

Permalink
Merge pull request #216 from hirosystems/beta
Browse files Browse the repository at this point in the history
release v0.7.0
  • Loading branch information
rafaelcr authored May 13, 2024
2 parents fe7c752 + b78560d commit bb32888
Show file tree
Hide file tree
Showing 31 changed files with 1,374 additions and 92 deletions.
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,45 @@
## [0.7.0-beta.5](https://github.com/hirosystems/token-metadata-api/compare/v0.7.0-beta.4...v0.7.0-beta.5) (2024-05-13)


### Bug Fixes

* improve image cache error handling ([#214](https://github.com/hirosystems/token-metadata-api/issues/214)) ([115a745](https://github.com/hirosystems/token-metadata-api/commit/115a745c268e7bb8115a488ca111e8b46cefed62))

## [0.7.0-beta.4](https://github.com/hirosystems/token-metadata-api/compare/v0.7.0-beta.3...v0.7.0-beta.4) (2024-05-08)


### Bug Fixes

* get access token properly ([a6b98c5](https://github.com/hirosystems/token-metadata-api/commit/a6b98c5099a9de1d88e74eed66dece1c4c157422))

## [0.7.0-beta.3](https://github.com/hirosystems/token-metadata-api/compare/v0.7.0-beta.2...v0.7.0-beta.3) (2024-05-07)


### Bug Fixes

* image cache agent arg types ([5826628](https://github.com/hirosystems/token-metadata-api/commit/5826628a329225fbf697a092dc201fc74fb96d43))

## [0.7.0-beta.2](https://github.com/hirosystems/token-metadata-api/compare/v0.7.0-beta.1...v0.7.0-beta.2) (2024-05-07)


### Bug Fixes

* reuse gcs token and validate image cache script errors ([#213](https://github.com/hirosystems/token-metadata-api/issues/213)) ([5e1af5c](https://github.com/hirosystems/token-metadata-api/commit/5e1af5c28cd0b1313f78a59b015669ceb07e5738))

## [0.7.0-beta.1](https://github.com/hirosystems/token-metadata-api/compare/v0.6.3...v0.7.0-beta.1) (2024-05-07)


### Features

* add admin rpc to reprocess token image cache ([#205](https://github.com/hirosystems/token-metadata-api/issues/205)) ([2fdcb33](https://github.com/hirosystems/token-metadata-api/commit/2fdcb33908062770da4e334810fd04bb378db66a))
* update ts client with image thumbnails ([#206](https://github.com/hirosystems/token-metadata-api/issues/206)) ([c24cb56](https://github.com/hirosystems/token-metadata-api/commit/c24cb56b854123b252eb2e2616bb8589c5b36f0f))
* upload token images to gcs ([#204](https://github.com/hirosystems/token-metadata-api/issues/204)) ([1cec219](https://github.com/hirosystems/token-metadata-api/commit/1cec2195a2b3df9e9c85f0152732594caa8c8c51))


### Bug Fixes

* get gcs auth token dynamically for image cache ([#210](https://github.com/hirosystems/token-metadata-api/issues/210)) ([8434b22](https://github.com/hirosystems/token-metadata-api/commit/8434b229f6d38e6799bf84bd6f1eb4de106996bb))

## [0.6.3](https://github.com/hirosystems/token-metadata-api/compare/v0.6.2...v0.6.3) (2024-05-07)


Expand Down
2 changes: 1 addition & 1 deletion client/typescript/.swagger-codegen/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.0.46
3.0.52
22 changes: 20 additions & 2 deletions client/typescript/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// tslint:disable
/**
* Token Metadata API
* Service that indexes metadata for every SIP-009, SIP-010, and SIP-013 Token in the Stacks blockchain and exposes it via REST API endpoints.
* Welcome to the API reference overview for the [Token Metadata API](https://docs.hiro.so/token-metadata-api). Service that indexes metadata for every SIP-009, SIP-010, and SIP-013 Token in the Stacks blockchain and exposes it via REST API endpoints.
*
* OpenAPI spec version: v0.4.0
*
Expand Down Expand Up @@ -71,7 +71,7 @@ export class BaseAPI {
* @extends {Error}
*/
export class RequiredError extends Error {
name: "RequiredError"
name = "RequiredError"
constructor(public field: string, msg?: string) {
super(msg);
}
Expand Down Expand Up @@ -250,6 +250,12 @@ export interface FtBasicMetadataResponse {
* @memberof FtBasicMetadataResponse
*/
image_uri?: string;
/**
*
* @type {string}
* @memberof FtBasicMetadataResponse
*/
image_thumbnail_uri?: string;
/**
*
* @type {string}
Expand Down Expand Up @@ -323,6 +329,12 @@ export interface FtMetadataResponse {
* @memberof FtMetadataResponse
*/
image_uri?: string;
/**
*
* @type {string}
* @memberof FtMetadataResponse
*/
image_thumbnail_uri?: string;
/**
*
* @type {string}
Expand Down Expand Up @@ -384,6 +396,12 @@ export interface Metadata {
* @memberof Metadata
*/
cached_image?: string;
/**
*
* @type {string}
* @memberof Metadata
*/
cached_thumbnail_image?: string;
/**
*
* @type {Array<MetadataAttribute>}
Expand Down
2 changes: 1 addition & 1 deletion client/typescript/api_test.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Token Metadata API
* Service that indexes metadata for every SIP-009, SIP-010, and SIP-013 Token in the Stacks blockchain and exposes it via REST API endpoints.
* Welcome to the API reference overview for the [Token Metadata API](https://docs.hiro.so/token-metadata-api). Service that indexes metadata for every SIP-009, SIP-010, and SIP-013 Token in the Stacks blockchain and exposes it via REST API endpoints.
*
* OpenAPI spec version: v0.4.0
*
Expand Down
2 changes: 1 addition & 1 deletion client/typescript/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// tslint:disable
/**
* Token Metadata API
* Service that indexes metadata for every SIP-009, SIP-010, and SIP-013 Token in the Stacks blockchain and exposes it via REST API endpoints.
* Welcome to the API reference overview for the [Token Metadata API](https://docs.hiro.so/token-metadata-api). Service that indexes metadata for every SIP-009, SIP-010, and SIP-013 Token in the Stacks blockchain and exposes it via REST API endpoints.
*
* OpenAPI spec version: v0.4.0
*
Expand Down
2 changes: 1 addition & 1 deletion client/typescript/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// tslint:disable
/**
* Token Metadata API
* Service that indexes metadata for every SIP-009, SIP-010, and SIP-013 Token in the Stacks blockchain and exposes it via REST API endpoints.
* Welcome to the API reference overview for the [Token Metadata API](https://docs.hiro.so/token-metadata-api). Service that indexes metadata for every SIP-009, SIP-010, and SIP-013 Token in the Stacks blockchain and exposes it via REST API endpoints.
*
* OpenAPI spec version: v0.4.0
*
Expand Down
4 changes: 2 additions & 2 deletions client/typescript/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion client/typescript/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hirosystems/token-metadata-api-client",
"version": "1.2.0",
"version": "1.3.0",
"description": "Client for @hirosystems/token-metadata-api",
"author": "Hiro Systems PBC <[email protected]> (https://hiro.so)",
"keywords": [
Expand Down
152 changes: 146 additions & 6 deletions config/image-cache.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,147 @@
#!/usr/bin/env node
const imgUrl = process.argv[2];
const encodedUrl = encodeURIComponent(imgUrl);
const [imgixDomain, imgixToken] = [process.env['IMGIX_DOMAIN'], process.env['IMGIX_TOKEN']];
const signature = require('crypto').createHash('md5').update(imgixToken + '/' + encodedUrl).digest('hex');
const resultUrl = new URL(encodedUrl + '?s=' + signature, imgixDomain);
console.log(resultUrl.toString());

/**
* This script is used to upload token metadata images to a Google Cloud Storage bucket. It also
* provides the option to resize an image to a max width before uploading so file sizes are more
* manageable upon display.
*
* The following arguments are taken in order from `argv`:
* * Remote image URL
* * Smart Contract principal
* * Token number
*
* Functionality can be tweaked with the following ENV vars:
* * `IMAGE_CACHE_MAX_BYTE_SIZE`: Max payload size accepted when downloading remote images.
* * `IMAGE_CACHE_RESIZE_WIDTH`: Width to resize images into while preserving aspect ratio.
* * `IMAGE_CACHE_GCS_BUCKET_NAME`: Google Cloud Storage bucket name. Example: 'assets.dev.hiro.so'
* * `IMAGE_CACHE_GCS_OBJECT_NAME_PREFIX`: Path for object storage inside the bucket. Example:
* 'token-metadata-api/mainnet/'
* * `IMAGE_CACHE_GCS_AUTH_TOKEN`: Google Cloud Storage authorization token. If undefined, the token
* will be fetched dynamically from Google.
* * `IMAGE_CACHE_CDN_BASE_PATH`: Base path for URLs that will be returned to the API for storage.
* Example: 'https://assets.dev.hiro.so/token-metadata-api/mainnet/'
*/

const sharp = require('sharp');
const { request, fetch, Agent } = require('undici');
const { Readable, PassThrough } = require('node:stream');

const IMAGE_URL = process.argv[2];
const CONTRACT_PRINCIPAL = process.argv[3];
const TOKEN_NUMBER = process.argv[4];

const IMAGE_RESIZE_WIDTH = parseInt(process.env['IMAGE_CACHE_RESIZE_WIDTH'] ?? '300');
const GCS_BUCKET_NAME = process.env['IMAGE_CACHE_GCS_BUCKET_NAME'];
const GCS_OBJECT_NAME_PREFIX = process.env['IMAGE_CACHE_GCS_OBJECT_NAME_PREFIX'];
const CDN_BASE_PATH = process.env['IMAGE_CACHE_CDN_BASE_PATH'];
const TIMEOUT = parseInt(process.env['METADATA_FETCH_TIMEOUT_MS'] ?? '30000');
const MAX_REDIRECTIONS = parseInt(process.env['METADATA_FETCH_MAX_REDIRECTIONS'] ?? '0');
const MAX_RESPONSE_SIZE = parseInt(process.env['IMAGE_CACHE_MAX_BYTE_SIZE'] ?? '-1');

async function getGcsAuthToken() {
const envToken = process.env['IMAGE_CACHE_GCS_AUTH_TOKEN'];
if (envToken !== undefined) return envToken;
try {
const response = await request(
'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token',
{
method: 'GET',
headers: { 'Metadata-Flavor': 'Google' },
throwOnError: true,
}
);
const json = await response.body.json();
// Cache the token so we can reuse it for other images.
process.env['IMAGE_CACHE_GCS_AUTH_TOKEN'] = json.access_token;
return json.access_token;
} catch (error) {
throw new Error(`GCS access token error: ${error}`);
}
}

async function upload(stream, name, authToken) {
await request(
`https://storage.googleapis.com/upload/storage/v1/b/${GCS_BUCKET_NAME}/o?uploadType=media&name=${GCS_OBJECT_NAME_PREFIX}${name}`,
{
method: 'POST',
body: stream,
headers: { 'Content-Type': 'image/png', Authorization: `Bearer ${authToken}` },
throwOnError: true,
}
);
return `${CDN_BASE_PATH}${name}`;
}

fetch(
IMAGE_URL,
{
dispatcher: new Agent({
headersTimeout: TIMEOUT,
bodyTimeout: TIMEOUT,
maxRedirections: MAX_REDIRECTIONS,
maxResponseSize: MAX_RESPONSE_SIZE,
throwOnError: true,
connect: {
rejectUnauthorized: false, // Ignore SSL cert errors.
},
}),
},
({ body }) => body
)
.then(async response => {
const imageReadStream = Readable.fromWeb(response.body);
const passThrough = new PassThrough();
const fullSizeTransform = sharp().png();
const thumbnailTransform = sharp()
.resize({ width: IMAGE_RESIZE_WIDTH, withoutEnlargement: true })
.png();
imageReadStream.pipe(passThrough);
passThrough.pipe(fullSizeTransform);
passThrough.pipe(thumbnailTransform);

let didRetryUnauthorized = false;
while (true) {
const authToken = await getGcsAuthToken();
try {
const results = await Promise.all([
upload(fullSizeTransform, `${CONTRACT_PRINCIPAL}/${TOKEN_NUMBER}.png`, authToken),
upload(thumbnailTransform, `${CONTRACT_PRINCIPAL}/${TOKEN_NUMBER}-thumb.png`, authToken),
]);
for (const r of results) console.log(r);
break;
} catch (error) {
if (
!didRetryUnauthorized &&
error.cause &&
error.cause.code == 'UND_ERR_RESPONSE_STATUS_CODE' &&
(error.cause.statusCode === 401 || error.cause.statusCode === 403)
) {
// GCS token is probably expired. Force a token refresh before trying again.
process.env['IMAGE_CACHE_GCS_AUTH_TOKEN'] = undefined;
didRetryUnauthorized = true;
} else throw error;
}
}
})
.catch(error => {
console.error(error);
// TODO: Handle `Input buffer contains unsupported image format` error from sharp when the image
// is actually a video or another media file.
let exitCode = 1;
if (
error.cause &&
(error.cause.code == 'UND_ERR_HEADERS_TIMEOUT' ||
error.cause.code == 'UND_ERR_BODY_TIMEOUT' ||
error.cause.code == 'UND_ERR_CONNECT_TIMEOUT' ||
error.cause.code == 'ECONNRESET')
) {
exitCode = 2;
} else if (
error.cause &&
error.cause.code == 'UND_ERR_RESPONSE_STATUS_CODE' &&
error.cause.statusCode === 429
) {
exitCode = 3;
}
process.exit(exitCode);
});
12 changes: 12 additions & 0 deletions migrations/1714500845265_thumbnail-images.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate';

export const shorthands: ColumnDefinitions | undefined = undefined;

export function up(pgm: MigrationBuilder): void {
pgm.addColumn('metadata', {
cached_thumbnail_image: {
type: 'text',
},
});
}
Loading

0 comments on commit bb32888

Please sign in to comment.