UNPKG

four-flap-meme-sdk

Version:

SDK for Flap bonding curve and four.meme TokenManager

196 lines (195 loc) 7.02 kB
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; }