four-flap-meme-sdk
Version:
SDK for Flap bonding curve and four.meme TokenManager
196 lines (195 loc) • 7.02 kB
JavaScript
import { JsonRpcProvider, Contract, Interface } from 'ethers';
import { ADDRESSES } from '../utils/constants.js';
const DEFAULT_GATEWAYS = {
BSC: 'https://ipfs.io/ipfs/',
BASE: 'https://ipfs.io/ipfs/',
XLAYER: 'https://ipfs.io/ipfs/',
MORPH: 'https://ipfs.io/ipfs/'
};
function normalizeCid(cidOrUrl) {
if (!cidOrUrl)
return cidOrUrl;
if (cidOrUrl.startsWith('ipfs://'))
return cidOrUrl.slice('ipfs://'.length);
return cidOrUrl;
}
async function fetchIpfsJson(cid, gateway) {
const url = `${gateway}${normalizeCid(cid)}`;
const resp = await fetch(url);
if (!resp.ok)
throw new Error(`IPFS fetch failed: ${resp.status} ${await resp.text()}`);
return await resp.json();
}
async function readCidFromToken(provider, tokenAddress) {
const abi = [
'function metaURI() view returns (string)',
'function meta() view returns (string)'
];
const c = new Contract(tokenAddress, abi, provider);
try {
const v = await c.metaURI();
if (typeof v === 'string' && v.length > 0)
return v;
}
catch { }
try {
const v = await c.meta();
if (typeof v === 'string' && v.length > 0)
return v;
}
catch { }
return undefined;
}
/**
* 根据代币地址直接读取合约 metaURI()/meta()(IPFS CID),并拉取元数据 JSON。
*/
export async function getFlapMetaByAddress(chain, rpcUrl, tokenAddress, opts = {}) {
const provider = new JsonRpcProvider(rpcUrl);
// 优先尝试:直接从代币合约读取 metaURI()/meta()
try {
const onChainCid = await readCidFromToken(provider, tokenAddress);
if (onChainCid) {
const gateway = opts.ipfsGateway || DEFAULT_GATEWAYS[chain];
try {
const data = await fetchIpfsJson(onChainCid, gateway);
return { cid: onChainCid, data };
}
catch {
return { cid: onChainCid };
}
}
}
catch { }
return undefined;
}
export async function getFlapMetasByAddresses(chain, rpcUrl, tokenAddresses, opts = {}) {
if (!tokenAddresses?.length)
return [];
const provider = new JsonRpcProvider(rpcUrl);
const uris = await getFlapMetaUrisWithMulticall(provider, chain, tokenAddresses, opts.multicall3, opts.ipfsGateway);
const gateway = opts.ipfsGateway || DEFAULT_GATEWAYS[chain];
const results = await Promise.all(uris.map(async (u) => {
if (!u.cid)
return { token: u.token, error: u.error };
if (u.data || u.imageUrl) {
return { token: u.token, cid: u.cid, data: u.data, imageUrl: u.imageUrl };
}
try {
const data = await fetchIpfsJson(u.cid, gateway);
return { token: u.token, cid: u.cid, data };
}
catch {
return { token: u.token, cid: u.cid, imageUrl: `${gateway}${normalizeCid(u.cid)}` };
}
}));
return results;
}
/**
* 使用 Multicall3 批量读取 metaURI()/meta(),metaURI 优先、meta 兜底。
*/
export async function getFlapMetaUrisWithMulticall(provider, chain, tokenAddresses, multicall3, ipfsGateway) {
// ✅ 所有链使用相同的 Multicall3 地址
const DEFAULT_MULTICALL3 = {
BSC: ADDRESSES.BSC.Multicall3,
BASE: ADDRESSES.BSC.Multicall3, // 所有链使用相同地址
XLAYER: ADDRESSES.BSC.Multicall3,
MORPH: ADDRESSES.BSC.Multicall3
};
const mcAddress = multicall3 || DEFAULT_MULTICALL3[chain];
const mcAbi = [
'function tryAggregate(bool requireSuccess, tuple(address target, bytes callData)[] calls) public returns (tuple(bool success, bytes returnData)[])'
];
const mc = new Contract(mcAddress, mcAbi, provider);
const iface = new Interface([
'function metaURI() view returns (string)',
'function meta() view returns (string)'
]);
// 第一轮:metaURI()
const callDataMetaURI = iface.encodeFunctionData('metaURI', []);
const calls1 = tokenAddresses.map((t) => ({ target: t, callData: callDataMetaURI }));
let res1 = [];
try {
res1 = await mc.tryAggregate(false, calls1);
}
catch (e) {
// 如果 multicall 不可用,降级为逐个查询
const fallbacks = await Promise.all(tokenAddresses.map(async (t) => {
try {
const cid = await readCidFromToken(provider, t);
return { token: t, cid };
}
catch (err) {
return { token: t, error: String(err?.message || err) };
}
}));
return fallbacks;
}
const needMeta = [];
const interim = tokenAddresses.map((t, i) => {
const r = res1[i];
if (r && r.success && r.returnData && r.returnData !== '0x') {
try {
const [uri] = iface.decodeFunctionResult('metaURI', r.returnData);
return { token: t, cid: String(uri) };
}
catch (e) {
needMeta.push(t);
return { token: t };
}
}
needMeta.push(t);
return { token: t };
});
if (needMeta.length === 0) {
// 尝试拉取 JSON,否则返回 imageUrl
const gateway = ipfsGateway || DEFAULT_GATEWAYS[chain];
const enriched = await Promise.all(interim.map(async (x) => {
if (!x.cid)
return x;
try {
const data = await fetchIpfsJson(x.cid, gateway);
return { ...x, data };
}
catch {
return { ...x, imageUrl: `${gateway}${normalizeCid(x.cid)}` };
}
}));
return enriched;
}
// 第二轮:meta() 兜底
const callDataMeta = iface.encodeFunctionData('meta', []);
const calls2 = needMeta.map((t) => ({ target: t, callData: callDataMeta }));
const res2 = await mc.tryAggregate(false, calls2);
let idx = 0;
for (let i = 0; i < interim.length; i++) {
if (interim[i].cid)
continue;
const r = res2[idx++];
if (r && r.success && r.returnData && r.returnData !== '0x') {
try {
const [uri] = iface.decodeFunctionResult('meta', r.returnData);
interim[i].cid = String(uri);
}
catch (e) {
interim[i].error = String(e?.message || e);
}
}
else {
interim[i].error = 'meta() call failed';
}
}
// 为所有项填充 data 或 imageUrl
const gateway = ipfsGateway || DEFAULT_GATEWAYS[chain];
const enriched = await Promise.all(interim.map(async (x) => {
if (!x.cid)
return x;
try {
const data = await fetchIpfsJson(x.cid, gateway);
return { ...x, data };
}
catch {
return { ...x, imageUrl: `${gateway}${normalizeCid(x.cid)}` };
}
}));
return enriched;
}