{"id":25369733,"url":"https://github.com/magickbase/gwscan-discord-bot","last_synced_at":"2025-11-09T23:03:47.215Z","repository":{"id":103000361,"uuid":"489757216","full_name":"Magickbase/gwscan-discord-bot","owner":"Magickbase","description":null,"archived":false,"fork":false,"pushed_at":"2025-10-25T23:59:53.000Z","size":199,"stargazers_count":1,"open_issues_count":11,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-10-30T05:47:25.290Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Magickbase.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2022-05-07T18:52:49.000Z","updated_at":"2022-05-27T08:48:43.000Z","dependencies_parsed_at":"2023-09-26T14:32:15.745Z","dependency_job_id":"93b5f8ba-5e1d-476b-9d6f-32d8e61a97cf","html_url":"https://github.com/Magickbase/gwscan-discord-bot","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Magickbase/gwscan-discord-bot","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Magickbase%2Fgwscan-discord-bot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Magickbase%2Fgwscan-discord-bot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Magickbase%2Fgwscan-discord-bot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Magickbase%2Fgwscan-discord-bot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Magickbase","download_url":"https://codeload.github.com/Magickbase/gwscan-discord-bot/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Magickbase%2Fgwscan-discord-bot/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":283593385,"owners_count":26861541,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-11-09T02:00:05.828Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-02-15T01:31:29.882Z","updated_at":"2025-11-09T23:03:47.199Z","avatar_url":"https://github.com/Magickbase.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"## Build a discord bot with GwScan Public API Service\n\n### Introduction to GwScan Public API Service\n\n[GwScan](https://www.gwscan.com/) is a blockchain explorer delivering data from Godwoken, backed by GwScan public API Service, a set of infrastructure to provide robust access to Godwoken, leaving developers to focus on their own products and to be a buidler.\n\n### Why you should build your application based on GwScan Public API Service\n\nGwScan Public API provides a set of well-grained APIs for DApp developers and concealed obscure concepts derived from Godwoken. It exposes APIs in GraphQL, which is more flexible and friendly for application development.\n\n### Quickstart\n\nWe'll go through the entire progress of building a discord bot with discord.js(Discord JavaScript SDK) and GwScan Public API Service to get you started.\n\n### Prerequisite\n\n1. A container with node.js runtime to serve discord bot, you may find the installation guide in https://nodejs.org;\n\n2. Discord developer account to authenticate your bot to discord servers. Specifically, we need\n    - Discord bot token, discord bot token is a set of chars used in authorization for the bot to perform functions on Discord client. You should go to https://discord.com/developers/applications and create an application, and then generate a fresh token on configuring your bot;\n    - Application id, also can be got in application's general information page, https://discord.com/developers/applications;\n    - Guild id: right click your discord server used to debug the bot and copy its id.\n\n3. A GwScan Public API service as a data source, you should deploy your own [GwScan](https://github.com/nervina-labs/godwoken_explorer) service. For convenience, we provide a public endpoint for godwoken v1.1 soon.\n\n#### Begin by creating an empty project\n\nWe should create an empty directory to start our project\n\n```sh\n$ mkdir gwscan-bot \u0026\u0026 cd gwscan-bot \u0026\u0026 npm init -y\n```\n\nCommands above will create an initial npm project with a name the same as that of directory.\n\nOur project will grow as follows,\n\n```sh\n.\n├── README.md\n├── lib # compiled files\n├── node_modules # npm dependencies\n├── package-lock.json # dependencies lock file\n├── package.json # project configuration\n├── src\n│   ├── client.ts # graphql client used to request data from gwscan public api service\n│   ├── commands\n│   │   ├── account.ts # /account [address] [private or public message]\n│   │   ├── index.ts\n│   │   └── transaction.ts # /transaction [transaction hash]\n│   ├── config.ts # project config, including discord bot token, application id, guild id\n│   ├── deploy-commands.ts # code to register commands to discord\n│   ├── index.ts\n│   └── utils\n│       ├── format.ts\n│       └── index.ts\n└── tsconfig.json # configuration of TypeScript\n```\n\n#### Add npm dependencies\n\n```sh\n$ npm install --save-exact @discordjs/builder @discordjs/rest  discord-api-types discord.js dotenv ts-node typescript bignumber.js graphql graphql-request\n```\n\n`@discordjs/builder`, `@discordjs/rest`, `discord-api-types` and `discord.js` are used to build discord commands;\n`dotenv`, `ts-node` and `typescript` are used to debug and compile our project;\n`bignumber.js`, `graphql` and `graphql-request` are used to build our main business logic.\n\n#### Configure our project with typescript and npm scripts\n\nAdd typescript config in `tsconfig.json`\n\n```json\n{\n  \"compilerOptions\": {\n    \"target\": \"es2016\",\n    \"module\": \"commonjs\",\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./lib\",\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true\n  }\n}\n```\n\nWe are going to add source code in `./src` and expect to compile it to `./lib`.\n\nAdd npm scripts in `package.jsno` to facilitate our work\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"ts-node src/index.ts\",\n    \"deploy\": \"ts-node src/deploy-commands\",\n    \"build\": \"tsc\"\n  }\n}\n```\n\n#### Import discord bot token and application id and guild id via dotenv\n\nAdd an environment variable file `.env` with the following content,\n\n```sh\nSERVER= # GwScan Public API Service Endpoint\nDISCORD_TOKEN= # Discord bot token\nAPP_ID= # application id\nGUILD_ID= # guild id\n```\n\nCreate `src/config.ts` and fill the following codes, it reads config from `.env`\n\n```typescript\nimport 'dotenv/config'\n\nexport const config = {\n  token: process.env.DISCORD_TOKEN,\n  publicKey: process.env.PUBLIC_KEY,\n  server: process.env.SERVER,\n  appId: process.env.APP_ID,\n  guildId: process.env.GUILD_ID,\n}\n\nexport const LOGO_URL = 'https://www.gwscan.com/icons/nervina-logo.svg'\nexport const GWSCAN_URL = 'https://www.gwscan.com'\nexport const CKB_DECIMALS = 8\nexport const PRIMARY_COLOR = '#E03C8A'\n```\n\n#### Create a graphql client object to fetch data from GwScan Public API Service\n\nCreate `src/client.ts`\n\n```typescript\nimport { GraphQLClient } from \"graphql-request\";\nimport { config } from './config'\n\nif (!config.server) {\n  throw new Error(`Server url is required in GraphQL client`)\n}\n\nexport const client = new GraphQLClient(config.server, { headers: { 'Accept': 'application/json' } })\n```\n\n#### Create commands\n\nWe will create two commands in this guide, `/account` and `/transaction`, to fetch basic info of a specific account or transaction as follows,\n![command_account](./docs/imgs/command_account.png)\n![command_transaction](./docs/imgs/command_transaction.png)\n\n##### Create account command\n\n```typescript\n// ./src/commands/account.ts\n\nimport { CacheType, CommandInteraction, MessageEmbed } from 'discord.js'\nimport { SlashCommandBuilder } from '@discordjs/builders'\nimport { gql } from 'graphql-request'\nimport { client } from '../client'\nimport { formatValue } from '../utils'\nimport { LOGO_URL, GWSCAN_URL, PRIMARY_COLOR } from '../config'\n\nconst query = gql`\n    query($address: String!) {\n      account(input: {address: $address}) {\n        id\n        eth_address\n        type\n        transaction_count\n        token_transfer_count\n        nonce\n        account_udts {\n          balance\n          udt {\n            id\n            icon\n            decimal\n            name\n            type\n            symbol\n          }\n        }\n      }\n    }\n  `\n\nexport const data = new SlashCommandBuilder()\n  .setName('account')\n  .setDescription('Replies account info')\n  .addStringOption(option =\u003e\n    option.setName('address')\n      .setDescription('Account eth address')\n      .setRequired(true))\n  .addStringOption(option =\u003e\n    option.setName('private')\n      .setDescription('Set visibility of this message')\n      .addChoices(\n        { name: 'invisible to channel', value: 'true' },\n        { name: 'visible to channel', value: 'false' }\n      )\n  )\n\nexport const execute = async (intereaction: CommandInteraction\u003cCacheType\u003e) =\u003e {\n  const address = intereaction.options.getString('address')\n  const isInvisible = intereaction.options.getString('private') === 'true'\n  const { account } = await client.request(query, { address })\n\n  if (!account) return intereaction.reply({ content: `Account \"${address}\" not found`, ephemeral: true })\n\n  const embed = new MessageEmbed()\n    .setColor(PRIMARY_COLOR)\n    .setImage(LOGO_URL)\n    .setThumbnail(LOGO_URL)\n    .setTitle(`${account.type} ${account.eth_address}`)\n    .setURL(`${GWSCAN_URL}/address/${account.eth_address}`)\n    .addFields(\n      account.account_udts.map((u: any) =\u003e ({\n        name: u.udt.name || 'Unknown',\n        value: formatValue(u.balance, u.udt?.decimal, u.udt?.symbol),\n        inline: true,\n      }))\n    )\n    .setTimestamp()\n\n  return intereaction.reply({ embeds: [embed], ephemeral: isInvisible })\n}\n```\n\nThere are 3 points to be noted,\n\n1. Declare a graphql query `query` to fetch account info\n\n```typescript\nconst query = gql`\n    query($address: String!) {\n      account(input: {address: $address}) {\n        id\n        eth_address\n        type\n        transaction_count\n        token_transfer_count\n        nonce\n        account_udts {\n          balance\n          udt {\n            id\n            icon\n            decimal\n            name\n            type\n            symbol\n          }\n        }\n      }\n    }\n  `\n```\n\nIt is a composable query based on schema designed in GwScan Public API Service and flexible for various applications, you can inspect the schema in [schema.graphql](https://github.com/Magickbase/godwoken_explorer/blob/main/docs/schema.graphql), or view it with a graphql explorer connected to the service.\n\nHere we query the basic fields of an account and get a response as follows,\n\n```json\n{\n \"data\": {\n  \"account\": {\n   \"account_udts\": [\n    {\n     \"balance\": \"204989529153\",\n     \"udt\": {\n      \"decimal\": 8,\n      \"icon\": \"https://cryptologos.cc/logos/nervos-network-ckb-logo.svg?v=022\",\n      \"id\": \"1\",\n      \"name\": \"CKB\",\n      \"symbol\": \"CKB\",\n      \"type\": \"BRIDGE\"\n     }\n    }\n   ],\n   \"eth_address\": \"0x387a3cb79141324c25ec4ea3c9b267b550c7fbdc\",\n   \"id\": 30690,\n   \"nonce\": 340302,\n   \"token_transfer_count\": 414290,\n   \"transaction_count\": 340250,\n   \"type\": \"USER\"\n  }\n }\n}\n```\n\n2. register a slash command of discord bot\n\n```typescript\nexport const data = new SlashCommandBuilder()\n  .setName('account')\n  .setDescription('Replies account info')\n  .addStringOption(option =\u003e\n    option.setName('address')\n      .setDescription('Account eth address')\n      .setRequired(true))\n  .addStringOption(option =\u003e\n    option.setName('private')\n      .setDescription('Set visibility of this message')\n      .addChoices(\n        { name: 'invisible to channel', value: 'true' },\n        { name: 'visible to channel', value: 'false' }\n      )\n  )\n```\n\n3. add handler of the slash command\n\n```typescript\nexport const execute = async (intereaction: CommandInteraction\u003cCacheType\u003e) =\u003e {\n  const address = intereaction.options.getString('address')\n  const isInvisible = intereaction.options.getString('private') === 'true'\n  const { account } = await client.request(query, { address })\n\n  if (!account) return intereaction.reply({ content: `Account \"${address}\" not found`, ephemeral: true })\n\n  const embed = new MessageEmbed()\n    .setColor(PRIMARY_COLOR)\n    .setImage(LOGO_URL)\n    .setThumbnail(LOGO_URL)\n    .setTitle(`${account.type} ${account.eth_address}`)\n    .setURL(`${GWSCAN_URL}/address/${account.eth_address}`)\n    .addFields(\n      account.account_udts.map((u: any) =\u003e ({\n        name: u.udt.name || 'Unknown',\n        value: formatValue(u.balance, u.udt?.decimal, u.udt?.symbol),\n        inline: true,\n      }))\n    )\n    .setTimestamp()\n\n  return intereaction.reply({ embeds: [embed], ephemeral: isInvisible })\n}\n```\n\nLet's do the same work for a transaction command,\n\n```typescript\nimport { CacheType, CommandInteraction, EmbedFieldData, MessageEmbed } from 'discord.js'\nimport { SlashCommandBuilder } from '@discordjs/builders'\nimport { gql } from 'graphql-request'\nimport { client } from '../client'\nimport { formatValue } from '../utils'\nimport { LOGO_URL, GWSCAN_URL, CKB_DECIMALS, PRIMARY_COLOR } from '../config'\n\nconst query = gql`\n    query ($hash: String!) {\n      transaction(input: { transaction_hash: $hash }) {\n        hash\n        from_account {\n          eth_address\n        }\n        to_account {\n          eth_address\n        }\n        type\n        polyjuice {\n          value\n          status\n          input\n        }\n        block {\n          number\n          hash\n          timestamp\n          status\n        }\n      }\n\n      token_transfers(input: { transaction_hash: $hash }) {\n        amount\n        from_address_hash\n        to_address_hash\n        udt {\n          id\n\t  decimal\n          symbol\n        }\n      }\n    }\n  `\n\nexport const data = new SlashCommandBuilder()\n  .setName('transaction')\n  .setDescription('Replies transaction info')\n  .addStringOption(option =\u003e\n    option.setName('hash')\n      .setDescription('Transaction hash')\n      .setRequired(true))\n\nexport const execute = async (intereaction: CommandInteraction\u003cCacheType\u003e) =\u003e {\n  const hash = intereaction.options.getString('hash')\n  const { transaction, token_transfers } = await client.request(query, { hash })\n\n  if (!transaction) return intereaction.reply({ content: `Transaction \"${hash}\" not found`, ephemeral: true })\n\n  const fields = [\n    { name: 'From', value: `[${transaction.from_account.eth_address}](${GWSCAN_URL}/address/${transaction.from_account.eth_address})` },\n    { name: 'To', value: `[${transaction.to_account.eth_address}](${GWSCAN_URL}/address/${transaction.to_account.eth_address})` },\n    transaction.polyjuice ? { name: 'Value', value: formatValue(transaction.polyjuice.value ?? '0', CKB_DECIMALS, 'CKB') } : null,\n    transaction.polyjuice ? { name: 'Tx Status', value: transaction.polyjuice.status } : null,\n    { name: 'Block', value: `${transaction.block?.number ?? '-'}` },\n    { name: 'Block Status', value: transaction.block?.status ?? '-' },\n    { name: \"Time\", value: transaction.block?.timestamp ? new Date(transaction.block.timestamp).toUTCString() : '-' },\n  ].filter(v =\u003e v) as Array\u003cEmbedFieldData\u003e\n\n  const embeds = [new MessageEmbed()\n    .setColor(PRIMARY_COLOR)\n    .setImage(LOGO_URL)\n    .setThumbnail(LOGO_URL)\n    .setTitle(`${transaction.type} Transaction\\n ${transaction.hash}`)\n    .setURL(`${GWSCAN_URL}/tx/${transaction.hash}`)\n    .addFields(...fields),\n\n  ...(token_transfers.map((t: any) =\u003e (\n    new MessageEmbed()\n      .setColor(PRIMARY_COLOR)\n      .setTitle(`Transfer ${formatValue(t.amount, t.udt?.decimal, t.udt?.symbol)}`)\n      .setFields(\n        { name: 'From', value: t.from_address_hash ? `[${t.from_address_hash}](${GWSCAN_URL}/address/${t.from_address_hash})` : '-' },\n        { name: 'To', value: t.to_address_hash ? `[${t.to_address_hash}](${GWSCAN_URL}/address/${t.to_address_hash})` : '-' }\n      )\n  )))\n  ]\n\n  return intereaction.reply({ embeds })\n}\n```\n\nThanks to composability of graphql, we can request transaction info and its internal transfers in a single command.\n\n```typescript\nconst query = gql`\n    query ($hash: String!) {\n      transaction(input: { transaction_hash: $hash }) {\n        hash\n        from_account {\n          eth_address\n        }\n        to_account {\n          eth_address\n        }\n        type\n        polyjuice {\n          value\n          status\n          input\n        }\n        block {\n          number\n          hash\n          timestamp\n          status\n        }\n      }\n\n      token_transfers(input: { transaction_hash: $hash }) {\n        amount\n        from_address_hash\n        to_address_hash\n        udt {\n          id\n\t\t\t\t\tdecimal\n          symbol\n        }\n      }\n    }\n  `\n```\n\nExpose these two commands in the module by\n\n```typescript\n// src/commands/index.ts\nexport * as account from './account'\nexport * as transaction from './transaction'\n```\n\n**We are almost there!**\n\n#### Register our commands of the bot\n\nCreate the script and run `npm run deploy` to call the npm script listed in the package.json.\n\n```typescript\nimport { REST } from '@discordjs/rest'\nimport { Routes } from 'discord-api-types/v9'\nimport { config } from './config'\nimport * as cmds from './commands'\n\nconst rest = new REST({ version: '9' }).setToken(config.token!)\nconst commands = Object.values(cmds).map(c =\u003e c.data.toJSON())\n\nrest.put(Routes.applicationGuildCommands(config.clientId!, config.guildId!), { body: commands })\n  .then(() =\u003e console.log(\"Deployed commands successfully\"))\n  .catch(console.error)\n```\n\nAfter this step, our bot will prompt user with registered commands when they type `/account` or `/transaction`.\n\n#### Deploy our bot\n\nOnce commands are registered, we should deploy our bot to handle requests from users.\n\nLet's add an entry of our program\n\n```typescript\n// src/index.ts\n\nimport { Client, Intents, Collection, CommandInteraction, CacheType } from 'discord.js'\nimport { config } from './config'\nimport * as cmds from './commands'\nimport { SlashCommandBuilder } from '@discordjs/builders'\n\nconst client = new Client({ intents: [Intents.FLAGS.GUILDS] })\n\nclient.once('ready', () =\u003e {\n  console.log('Ready')\n})\n\nconst commands = new Collection\u003cstring, { data: Omit\u003cSlashCommandBuilder, \"addSubcommandGroup\" | \"addSubcommand\"\u003e, execute: (i: CommandInteraction\u003cCacheType\u003e) =\u003e Promise\u003cvoid\u003e }\u003e()\n\n// cache all commands and their callback in `commands` map\nObject.entries(cmds).forEach(([name, value]) =\u003e commands.set(name, value))\n\nclient.on('interactionCreate', async (interaction) =\u003e {\n  if (!interaction.isCommand()) return\n\n  const command = commands.get(interaction.commandName)\n  if (!command) return\n\n  try {\n    // handle requests from users\n    await command.execute(interaction)\n  } catch (err) {\n    console.error(err)\n    await interaction.reply({\n      content: `There was an error on executing this command`, ephemeral: true\n    })\n  }\n})\n\nclient.login(config.token)\n```\n\nNow, we can try the bot by running it in dev mode `npm run dev`. Once it's up, you can type `/account` or `/transaction` in your develop discord server where bot has been invited into and wait for the response.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmagickbase%2Fgwscan-discord-bot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmagickbase%2Fgwscan-discord-bot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmagickbase%2Fgwscan-discord-bot/lists"}