cyb/scripts/pin-icons-to-pinata.ts

/**
 * Скрипт для пининга иконок токенов и сетей на Pinata.
 *
 * Запуск:
 *   DENO_NO_PACKAGE_JSON=1 deno run --allow-net --allow-read --allow-env scripts/pin-icons-to-pinata.ts
 *
 * Требует .env с:
 *   PINATA_API_KEY, PINATA_API_SECRET, PINATA_GATEWAY
 */

const LCD_URL = "https://lcd.bostrom.cybernode.ai";
const HUB_TOKENS = "bostrom15phze6xnvfnpuvvgs2tw58xnnuf872wlz72sv0j2yauh6zwm7cmqqpmc42";
const HUB_NETWORKS = "bostrom1lpn69a74ftv04upfej8f9ay56pe2zyk48vzlk49kp3grysc7u56qq363nr";

// --- Load .env ---
async function loadEnv(): Promise<Record<string, string>> {
  const env: Record<string, string> = {};
  try {
    const text = await Deno.readTextFile(".env");
    for (const line of text.split("\n")) {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith("#")) continue;
      const eqIdx = trimmed.indexOf("=");
      if (eqIdx > 0) {
        env[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1);
      }
    }
  } catch {
    console.error("Не найден файл .env");
    Deno.exit(1);
  }
  return env;
}

// --- Query smart contract ---
async function queryContract(contractAddr: string): Promise<any> {
  const queryMsg = btoa(JSON.stringify({ get_entries: {} }));
  const url = `${LCD_URL}/cosmwasm/wasm/v1/contract/${contractAddr}/smart/${queryMsg}`;
  const resp = await fetch(url);
  if (!resp.ok) throw new Error(`Query failed: ${resp.status} ${await resp.text()}`);
  const json = await resp.json();
  return json.data;
}

// --- Pin CID on Pinata by hash ---
async function pinByHash(
  cid: string,
  name: string,
  apiKey: string,
  apiSecret: string
): Promise<boolean> {
  const resp = await fetch("https://api.pinata.cloud/pinning/pinByHash", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      pinata_api_key: apiKey,
      pinata_secret_api_key: apiSecret,
    },
    body: JSON.stringify({
      hashToPin: cid,
      pinataMetadata: { name },
    }),
  });

  if (resp.ok) {
    return true;
  }

  const text = await resp.text();
  // Already pinned
  if (text.includes("DUPLICATE_OBJECT") || text.includes("already being tracked")) {
    return true;
  }

  console.error(`  Ошибка пина ${cid}: ${resp.status} ${text}`);
  return false;
}

// --- Main ---
async function main() {
  const env = await loadEnv();
  const apiKey = env.PINATA_API_KEY;
  const apiSecret = env.PINATA_API_SECRET;
  const gateway = env.PINATA_GATEWAY;

  if (!apiKey || !apiSecret) {
    console.error("PINATA_API_KEY и PINATA_API_SECRET нужны в .env");
    Deno.exit(1);
  }

  console.log("Загружаю токены из контракта...");
  const tokensData = await queryContract(HUB_TOKENS);
  const tokens = tokensData.entries || [];

  console.log("Загружаю сети из контракта...");
  const networksData = await queryContract(HUB_NETWORKS);
  const networks = networksData.entries || [];

  // Collect unique CIDs
  const cids = new Map<string, string>(); // cid -> name
  for (const token of tokens) {
    if (token.logo) {
      cids.set(token.logo, `token-${token.ticker}`);
    }
  }
  for (const network of networks) {
    if (network.logo) {
      cids.set(network.logo, `network-${network.name || network.chain_id}`);
    }
  }

  console.log(`\nНайдено ${cids.size} уникальных CID иконок\n`);

  let pinned = 0;
  let failed = 0;

  for (const [cid, name] of cids) {
    console.log(`Пиню: ${name} (${cid})`);
    const ok = await pinByHash(cid, name, apiKey, apiSecret);
    if (ok) {
      pinned++;
      if (gateway) {
        console.log(`  ✓ https://${gateway}/ipfs/${cid}`);
      } else {
        console.log(`  ✓ запинено`);
      }
    } else {
      failed++;
    }
  }

  console.log(`\nГотово: ${pinned} запинено, ${failed} ошибок`);
  if (gateway) {
    console.log(`Gateway: https://${gateway}/ipfs/<CID>`);
  }
}

main();

Neighbours