Skip to content

Commit

Permalink
🔀 Merge pull request #296 from williamchong/feature/likernft
Browse files Browse the repository at this point in the history
Feature/likernft
  • Loading branch information
williamchong authored Jul 27, 2022
2 parents 7744263 + cc8e83b commit 5d6231c
Show file tree
Hide file tree
Showing 32 changed files with 3,164 additions and 673 deletions.
16 changes: 16 additions & 0 deletions config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,37 @@ config.FIRESTORE_PAYOUT_ROOT = process.env.FIRESTORE_PAYOUT_ROOT;
config.FIRESTORE_MISSION_ROOT = process.env.FIRESTORE_MISSION_ROOT;
config.FIRESTORE_CONFIG_ROOT = process.env.FIRESTORE_CONFIG_ROOT;
config.FIRESTORE_COUPON_ROOT = process.env.FIRESTORE_COUPON_ROOT;
config.FIRESTORE_LIKER_NFT_ROOT = process.env.FIRESTORE_LIKER_NFT_ROOT;
config.FIRESTORE_OAUTH_CLIENT_ROOT = process.env.FIRESTORE_OAUTH_CLIENT_ROOT;
config.FIREBASE_STORAGE_BUCKET = process.env.FIREBASE_STORAGE_BUCKET;
config.FIRESTORE_ISCN_INFO_ROOT = process.env.FIRESTORE_ISCN_INFO_ROOT;

config.COSMOS_LCD_INDEXER_ENDPOINT = 'https://node.testnet.like.co';
config.COSMOS_LCD_ENDPOINT = 'https://node.testnet.like.co';
config.COSMOS_RPC_ENDPOINT = 'https://node.testnet.like.co/rpc/';
config.COSMOS_SIGNING_RPC_ENDPOINT = 'https://node.testnet.like.co/rpc/';
config.COSMOS_CHAIN_ID = 'likecoin-public-testnet-5';
config.ISCN_DEV_LCD_ENDPOINT = 'localhost:1317';
config.ISCN_DEV_CHAIN_ID = 'iscn-dev-chain';
config.COSMOS_DENOM = 'nanoekil';
config.NFT_RPC_ENDPOINT = 'https://node.testnet.like.co';
config.NFT_SIGNING_RPC_ENDPOINT = 'https://node.testnet.like.co';
config.NFT_CHAIN_ID = 'likecoin-public-testnet-5';
config.NFT_COSMOS_DENOM = 'nanoekil';

config.ARWEAVE_LIKE_TARGET_ADDRESS = '';
config.IPFS_ENDPOINT = 'https://ipfs.infura.io:5001/api/v0';
config.REPLICA_IPFS_ENDPOINTS = [];

config.LIKER_NFT_TARGET_ADDRESS = '';
config.LIKER_NFT_STARTING_PRICE = 8;
config.LIKER_NFT_PRICE_MULTIPLY = 2;
config.LIKER_NFT_PRICE_DECAY = 0.2;
config.LIKER_NFT_DECAY_START_BATCH = 13;
config.LIKER_NFT_DECAY_END_BATCH = 18;
config.LIKER_NFT_GAS_FEE = '200000';


config.AUTHCORE_API_ENDPOINT = '';
config.AUTHCORE_PUBLIC_CERT_PATH = '';
config.AUTHCORE_PRIVATE_KEY_PATH = '';
Expand Down
1 change: 1 addition & 0 deletions config/secret.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const config = {};

config.COSMOS_PRIVATE_KEY = Buffer.from('1111111111111111111111111111111111111111111111111111111111111111', 'hex');
config.LIKER_NFT_PRIVATE_KEY = Buffer.from('1111111111111111111111111111111111111111111111111111111111111111', 'hex');

module.exports = config;
2,201 changes: 1,553 additions & 648 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@
]
},
"dependencies": {
"@cosmjs/stargate": "^0.26.1",
"@cosmjs/stargate": "^0.28.4",
"@google-cloud/pubsub": "^1.2.0",
"@likecoin/iscn-js": "0.0.7",
"@likecoin/iscn-js": "0.2.0-rc.1",
"@likecoin/likecoin-email-templates": "^1.2.2",
"@likecoin/secretd-js": "^0.4.3",
"@sendgrid/mail": "^6.5.5",
Expand All @@ -60,7 +60,7 @@
"compression": "^1.7.3",
"cookie-parser": "^1.4.3",
"cors": "^2.8.5",
"cosmjs-types": "^0.2.0",
"cosmjs-types": "^0.5.0",
"create-hash": "^1.2.0",
"disposable-email-domains": "^1.0.56",
"eth-sig-util": "^2.5.4",
Expand All @@ -70,7 +70,7 @@
"express-rate-limit": "^3.3.2",
"fast-json-stable-stringify": "^2.1.0",
"file-type": "^12.4.2",
"firebase-admin": "^8.8.0",
"firebase-admin": "^9.12.0",
"i18n": "^0.8.3",
"ipfs-http-client": "^53.0.1",
"ipfs-only-hash": "^4.0.0",
Expand Down
Binary file added src/assets/book.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/iscn.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/constant/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export const LOGIN_MESSAGE = 'Login - Reinventing the Like';

export const EXTERNAL_HOSTNAME = process.env.EXTERNAL_HOSTNAME || (IS_TESTNET ? 'rinkeby.like.co' : 'like.co');

export const API_EXTERNAL_HOSTNAME = process.env.API_EXTERNAL_HOSTNAME || `api.${EXTERNAL_HOSTNAME}`;

export const GETTING_STARTED_TASKS = ['taskSocial', 'taskOnepager', 'taskVideo', 'taskPaymentPage'];

export const TRANSACTION_QUERY_LIMIT = 10;
Expand Down Expand Up @@ -139,3 +141,7 @@ export const LIKE_DEFAULT_PRICE = 0.0082625;
export const KICKBOX_DISPOSIBLE_API = 'https://open.kickbox.com/v1/disposable';

export const COINGECKO_AR_LIKE_PRICE_API = 'https://api.coingecko.com/api/v3/simple/price?ids=arweave,likecoin&vs_currencies=usd';

export const LIKECOIN_DARK_GREEN_THEME_COLOR = '#28646E';

export const APP_LIKE_CO_ISCN_VIEW_URL = `https://app.${IS_TESTNET ? 'rinkeby.' : ''}like.co/view/`;
26 changes: 26 additions & 0 deletions src/middleware/likernft.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ValidationError } from '../util/ValidationError';
import {
getISCNIdByClassId, getCurrentClassIdByISCNId, getISCNPrefixDocName,
} from '../util/api/likernft';

export const fetchISCNIdAndClassId = async (req, res, next) => {
try {
let { iscn_id: iscnId, class_id: classId } = req.query;
if (!iscnId && !classId) throw new ValidationError('MISSING_ISCN_OR_CLASS_ID');

if (!iscnId) {
iscnId = await getISCNIdByClassId(classId);
}
if (!classId) {
classId = await getCurrentClassIdByISCNId(iscnId);
}
res.locals.iscnId = iscnId;
res.locals.classId = classId;
res.locals.iscnPrefix = getISCNPrefixDocName(iscnId);
next();
} catch (err) {
next(err);
}
};

export default fetchISCNIdAndClassId;
2 changes: 2 additions & 0 deletions src/routes/getPublicInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import oembed from './oembed';
import cosmos from './cosmos';
import arweave from './arweave';
import iscn from './iscn';
import likernft from './likernft';

const router = Router();

Expand All @@ -31,5 +32,6 @@ router.use('/oembed', oembed);
router.use('/cosmos', cosmos);
router.use('/arweave', arweave);
router.use('/iscn', iscn);
router.use('/likernft', likernft);

export default router;
83 changes: 83 additions & 0 deletions src/routes/likernft/history.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Router } from 'express';
import axios from 'axios';
import { ValidationError } from '../../util/ValidationError';
import { getISCNDocByClassId } from '../../util/api/likernft';
import { fetchISCNIdAndClassId } from '../../middleware/likernft';
import { COSMOS_LCD_INDEXER_ENDPOINT } from '../../../config/config';

const router = Router();

router.get(
'/history',
fetchISCNIdAndClassId,
async (req, res, next) => {
try {
const { nft_id: nftId } = req.query;
const { classId } = res.locals;
if (nftId && !classId) {
throw new ValidationError('PLEASE_DEFINE_CLASS_ID');
}
let list = [];
const doc = await getISCNDocByClassId(classId);
let queryObj = await doc.ref.collection('transaction');
if (nftId) queryObj = queryObj.where('nftId', '==', nftId);
const query = await queryObj.orderBy('timestamp', 'desc').get();
list = query.docs.map(d => ({ txHash: d.id, ...(d.data() || {}) }));
res.json({
list,
});
} catch (err) {
next(err);
}
},
);

router.get(
'/events',
fetchISCNIdAndClassId,
async (_, res, next) => {
try {
const { classId } = res.locals;
const [
{ data: newClassData },
{ data: mintData },
{ data: sendData },
] = await Promise.all([
axios.get(`${COSMOS_LCD_INDEXER_ENDPOINT}/cosmos/tx/v1beta1/txs?events=likechain.likenft.v1.EventNewClass.class_id=%27%22${classId}%22%27`),
axios.get(`${COSMOS_LCD_INDEXER_ENDPOINT}/cosmos/tx/v1beta1/txs?events=likechain.likenft.v1.EventMintNFT.class_id=%27%22${classId}%22%27`),
axios.get(`${COSMOS_LCD_INDEXER_ENDPOINT}/cosmos/tx/v1beta1/txs?events=cosmos.nft.v1beta1.EventSend.class_id=%27%22${classId}%22%27`),
]);
let list = [];
list = list.concat(newClassData.tx_responses || []);
let set = new Set(list.map(t => t.txhash));
list = list.concat((mintData.tx_responses || []).filter(t => !set.has(t.txhash)));
set = new Set(list.map(t => t.txhash));
list = list.concat((sendData.tx_responses || []).filter(t => !set.has(t.txhash)));
list = list.map((d) => {
const {
height,
txhash,
code,
logs,
tx,
timestamp,
} = d;
return {
height,
txhash,
code,
logs,
tx,
timestamp,
};
}).sort((a, b) => b.timestamp - a.timestamp);
res.json({
list,
});
} catch (err) {
next(err);
}
},
);

export default router;
12 changes: 12 additions & 0 deletions src/routes/likernft/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as fs from 'fs';
import { Router } from 'express';

const router = Router();

fs.readdirSync(__dirname).forEach((file) => {
const name = file.split('.')[0];
if (!name || name === 'index') return;
router.use(require(`./${name}`).default); // eslint-disable-line import/no-dynamic-require,global-require
});

export default router;
118 changes: 118 additions & 0 deletions src/routes/likernft/metadata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Router } from 'express';
import axios from 'axios';
import { ONE_DAY_IN_S, API_EXTERNAL_HOSTNAME } from '../../constant';
import { likeNFTCollection, iscnInfoCollection } from '../../util/firebase';
import { filterLikeNFTMetadata } from '../../util/ValidationHelper';
import { getISCNIdByClassId } from '../../util/api/likernft';
import {
getLikerNFTDynamicData, getBasicImage, getCombinedImage, getResizedImage,
} from '../../util/api/likernft/metadata';
import { getNFTISCNData, getNFTClassDataById, getNFTOwner } from '../../util/cosmos/nft';
import { fetchISCNIdAndClassId } from '../../middleware/likernft';
import { getISCNPrefix } from '../../util/cosmos/iscn';
import { LIKER_NFT_TARGET_ADDRESS } from '../../../config/config';
import { ValidationError } from '../../util/ValidationError';
import { sleep } from '../../util/misc';

const router = Router();

router.get(
'/metadata',
fetchISCNIdAndClassId,
async (_, res, next) => {
try {
const { classId, iscnId, iscnPrefix } = res.locals;
const classDocRef = likeNFTCollection.doc(iscnPrefix).collection('class').doc(classId);

const classDoc = await classDocRef.get();
const classData = classDoc.data();
if (!classData) {
res.status(404).send('NFT_DATA_NOT_FOUND');
return;
}
const [{ owner: iscnOwner, data: iscnData }, chainData, dynamicData] = await Promise.all([
getNFTISCNData(iscnId).catch((err) => { console.error(err); return {}; }),
getNFTClassDataById(classId).catch(err => console.error(err)),
getLikerNFTDynamicData(classId, classData).catch(err => console.error(err)),
]);
if (!iscnData) throw new ValidationError('ISCN_NOT_FOUND');
if (!chainData) throw new ValidationError('NFT_CLASS_NOT_FOUND');
if (!dynamicData) throw new ValidationError('NFT_CLASS_NOT_REGISTERED');

res.set('Cache-Control', `public, max-age=${60}, s-maxage=${60}, stale-if-error=${ONE_DAY_IN_S}`);
res.json(filterLikeNFTMetadata({
iscnId: getISCNPrefix(iscnId),
iscnOwner,
iscnStakeholders: iscnData.stakeholders,
...(classData.metadata || {}),
...chainData,
...dynamicData,
}));
} catch (err) {
next(err);
}
},
);

router.get(
'/metadata/owners',
fetchISCNIdAndClassId,
async (_, res, next) => {
try {
const { classId, iscnPrefix } = res.locals;
const nftQuery = await likeNFTCollection.doc(iscnPrefix)
.collection('class').doc(classId)
.collection('nft')
.where('ownerWallet', '!=', LIKER_NFT_TARGET_ADDRESS)
.get();
const nftIds = nftQuery.docs.map(n => n.id);
const ownerMap = {
// don't include api holded wallet
// LIKER_NFT_TARGET_ADDRESS: apiOwnedNFTIds,
};
const owners = await Promise.all(nftIds.map(id => getNFTOwner(classId, id)));
owners.forEach((owner, index) => {
ownerMap[owner] = ownerMap[owner] || [];
ownerMap[owner].push(nftIds[index]);
});
res.set('Cache-Control', `public, max-age=${60}, s-maxage=${60}, stale-if-error=${ONE_DAY_IN_S}`);
res.json(ownerMap);
} catch (err) {
next(err);
}
},
);

router.get(
'/metadata/image/class_(:classId)(.png)?',
async (req, res, next) => {
try {
const { classId } = req.params;
const iscnId = await getISCNIdByClassId(classId);
let iscnData = await iscnInfoCollection.doc(encodeURIComponent(iscnId)).get();
if (!iscnData.exists) {
await axios.post(
`https://${API_EXTERNAL_HOSTNAME}/like/info`,
{ iscnId },
);
await sleep(1000);
iscnData = await iscnInfoCollection.doc(encodeURIComponent(iscnId)).get();
}
let image = '';
let title = '';
if (iscnData.exists) {
({ image, title } = iscnData.data());
}
const basicImage = await getBasicImage(image, title);
const resizedImage = getResizedImage();
const combinedImage = await getCombinedImage();
res.set('Cache-Control', `public, max-age=${60}, s-maxage=${60}, stale-if-error=${ONE_DAY_IN_S}`);
basicImage.pipe(resizedImage).pipe(combinedImage).pipe(res);
return;
} catch (err) {
next(err);
}
},
);

export default router;
Loading

0 comments on commit 5d6231c

Please sign in to comment.