Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

release new beta #212

Merged
merged 5 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
118 changes: 112 additions & 6 deletions config/image-cache.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,113 @@
#!/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'];

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' },
}
);
if (response.data?.access_token) return response.data.access_token;
throw new Error(`GCS token not found`);
} catch (error) {
throw new Error(`Error fetching GCS access token: ${error.message}`);
}
}

async function upload(stream, name, authToken) {
try {
const response = 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}` },
}
);
if (response.statusCode !== 200) throw new Error(`GCS error: ${response.statusCode}`);
return `${CDN_BASE_PATH}${name}`;
} catch (error) {
throw new Error(`Error uploading ${name}: ${error.message}`);
}
}

fetch(
IMAGE_URL,
{
dispatcher: new Agent({
headersTimeout: process.env['METADATA_FETCH_TIMEOUT_MS'],
bodyTimeout: process.env['METADATA_FETCH_TIMEOUT_MS'],
maxRedirections: process.env['METADATA_FETCH_MAX_REDIRECTIONS'],
maxResponseSize: process.env['IMAGE_CACHE_MAX_BYTE_SIZE'],
connect: {
rejectUnauthorized: false, // Ignore SSL cert errors.
},
}),
},
({ statusCode, body }) => {
if (statusCode !== 200) throw new Error(`Failed to fetch image: ${statusCode}`);
return 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);

const authToken = await getGcsAuthToken();
const results = await Promise.all([
upload(fullSizeTransform, `${CONTRACT_PRINCIPAL}/${TOKEN_NUMBER}.png`, authToken),
upload(thumbnailTransform, `${CONTRACT_PRINCIPAL}/${TOKEN_NUMBER}-thumb.png`, authToken),
]);

// The API will read these strings as CDN URLs.
for (const result of results) console.log(result);
})
.catch(error => {
throw new Error(`Error processing image: ${error.message}`);
});
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
Loading