Repository: solana-labs/chatgpt-plugin
Branch: master
Commit: 6b7c56e757c0
Files: 77
Total size: 225.9 KB
Directory structure:
gitextract_4oy13ijj/
├── .gitignore
├── DISCLAIMER.md
├── LICENSE
├── README.md
├── chatgpt-plugin/
│ ├── .gitignore
│ ├── .prettierrc
│ ├── README.md
│ ├── next.config.js
│ ├── postcss.config.js
│ ├── public/
│ │ └── openapi.yaml
│ ├── src/
│ │ ├── app/
│ │ │ ├── globals.css
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── lib/
│ │ │ ├── address.ts
│ │ │ ├── features.tsx
│ │ │ ├── helloMoon/
│ │ │ │ └── index.ts
│ │ │ ├── hyperspace/
│ │ │ │ ├── account.ts
│ │ │ │ ├── constants.ts
│ │ │ │ └── idl/
│ │ │ │ └── hyperspace.ts
│ │ │ ├── middleware.ts
│ │ │ └── on-chain-metadata/
│ │ │ └── index.ts
│ │ └── pages/
│ │ └── api/
│ │ ├── constants.ts
│ │ ├── handlers/
│ │ │ ├── getAccountInfo/
│ │ │ │ └── index.ts
│ │ │ ├── getAssetsByOwner/
│ │ │ │ └── index.ts
│ │ │ ├── getBalance/
│ │ │ │ └── index.ts
│ │ │ ├── getCollectionsByFloorPrice/
│ │ │ │ └── index.ts
│ │ │ ├── getCollectionsByName/
│ │ │ │ └── index.ts
│ │ │ ├── getListedCollectionNFTs/
│ │ │ │ └── index.ts
│ │ │ ├── getSignaturesForAddress/
│ │ │ │ └── index.ts
│ │ │ ├── getTokenAccounts/
│ │ │ │ └── index.ts
│ │ │ ├── getTotalValue/
│ │ │ │ └── index.ts
│ │ │ ├── getTransaction/
│ │ │ │ └── index.ts
│ │ │ ├── solana-pay/
│ │ │ │ ├── page/
│ │ │ │ │ └── [methodName].ts
│ │ │ │ ├── qr/
│ │ │ │ │ └── [methodName].ts
│ │ │ │ ├── sign/
│ │ │ │ │ ├── createBurnAsset.ts
│ │ │ │ │ ├── createBuyNFT.ts
│ │ │ │ │ ├── createCloseNFTMetadata.ts
│ │ │ │ │ ├── createListNFT.ts
│ │ │ │ │ ├── createMerkleTree.ts
│ │ │ │ │ ├── createMintCNFT.ts
│ │ │ │ │ ├── createMintNFT.ts
│ │ │ │ │ ├── createSetProfilePic.ts
│ │ │ │ │ ├── createTransferAsset.ts
│ │ │ │ │ ├── createTransferSol.ts
│ │ │ │ │ ├── createTransferToken.ts
│ │ │ │ │ ├── createWriteNFTMetadata.ts
│ │ │ │ │ └── index.ts
│ │ │ │ └── utils/
│ │ │ │ └── helpers.ts
│ │ │ ├── tokenName/
│ │ │ │ └── index.ts
│ │ │ ├── tx-link/
│ │ │ │ └── [txSlug].ts
│ │ │ └── walletName/
│ │ │ └── index.ts
│ │ ├── helloMoon/
│ │ │ ├── defi/
│ │ │ │ ├── defi.yaml
│ │ │ │ ├── programNewUsers.ts
│ │ │ │ ├── programOverlap.ts
│ │ │ │ ├── tokenStats.ts
│ │ │ │ └── tokenUsers.ts
│ │ │ ├── jupiter/
│ │ │ │ ├── README.md
│ │ │ │ ├── historical.ts
│ │ │ │ ├── pairs.ts
│ │ │ │ ├── summary.ts
│ │ │ │ └── swaps.ts
│ │ │ └── jupiter.yaml
│ │ ├── metaplex/
│ │ │ ├── getCNFTRent/
│ │ │ │ └── index.ts
│ │ │ ├── getPublicTree/
│ │ │ │ └── index.ts
│ │ │ └── metaplex.yaml
│ │ ├── solflare-pfp/
│ │ │ └── getProfilePic.ts
│ │ └── tiplink/
│ │ └── makeLink.ts
│ └── tailwind.config.js
├── langchain_examples/
│ ├── README.md
│ └── python/
│ └── main.py
├── pyproject.toml
└── scripts/
├── createNFTCollection.ts
├── createTree.ts
├── helper.ts
├── runCreateCollection.ts
├── send.ts
└── uploadMetadata.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.DS_Store
.env
node_modules/
yarn*
tmp*
# Debugging tools
scripts/collection.ts
scripts/sharp.ts
langchain_examples/python/__pycache__
*json
================================================
FILE: DISCLAIMER.md
================================================
# Disclaimer
All claims, content, designs, algorithms, estimates, roadmaps, specifications, and performance measurements described in this project are done with the good faith efforts Solana Labs, Inc. and its affiliates ("SL"). It is up to the reader to check and validate their accuracy and truthfulness. Furthermore nothing in this project constitutes a solicitation for investment.
Any content produced by SL or developer resources that SL provides have not been subject to audit and are for educational and inspiration purposes only. SL does not encourage, induce or sanction the deployment, integration or use of any such applications (including the code comprising the Solana blockchain protocol) in violation of applicable laws or regulations and hereby prohibits any such deployment, integration or use. This includes use of any such applications by the reader (a) in violation of export control or sanctions laws of the United States or any other applicable jurisdiction, (b) if the reader is located in or ordinarily resident in a country or territory subject to comprehensive sanctions administered by the U.S. Office of Foreign Assets Control (OFAC), or (c) if the reader is or is working on behalf of a Specially Designated National (SDN) or a person subject to similar blocking or denied party prohibitions.
The reader should be aware that U.S. export control and sanctions laws prohibit U.S. persons (and other persons that are subject to such laws) from transacting with persons in certain countries and territories or that are on the SDN list. As a project based primarily on open-source software, it is possible that such sanctioned persons may nevertheless bypass prohibitions, obtain the code comprising the Solana blockchain protocol (or other project code or applications) and deploy, integrate, or otherwise use it. Accordingly, there is a risk to individuals that other persons using the Solana blockchain protocol may be sanctioned persons and that transactions with such persons would be a violation of U.S. export controls and sanctions law. This risk applies to individuals, organizations, and other ecosystem participants that deploy, integrate, or use the Solana blockchain protocol code directly (e.g., as a node operator), and individuals that transact on the Solana blockchain through light clients, third party interfaces, and/or wallet software.
================================================
FILE: LICENSE
================================================
Copyright 2023 Solana Labs, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# Solana ChatGPT Plugin
A ChatGPT plugin for Solana. Install as an unverified plugin with url `https://chatgpt.solanalabs.com`.
## Endpoints
ChatGPT can POST to the following resources, as described by `.well-known/openapi.yaml`.
/getAccountInfo { address }
Returns the output of `getAccountInfo` method from the RPC with buffer data, and if it can be deserialized by its program IDL, then the response payload has additional field called `extended` that has a JSON serialized string of the anchor data. Chat GPT's plugin model seems to be able to read this pretty well.
```json
{
...,
"extended": "{\"authority\":\"8fbqVvpK3Dj7fdP2c8JJhtD7Zy3n9qtwAeGfbkgPu625\",\"numMinted\":50}"
}
```
/getBalance { address }
Returns
```json
{
"sol": 0.40296
}
```
/getAssetsByOwner { address }
Returns the assets returned by the [Metaplex Read API spec](https://github.com/metaplex-foundation/api-specifications/blob/main/specifications/read_api/openrpc_spec.json)
/getTransaction { signature }
Accepts
```json
{
"signature": "h51pjmFcn8LkxejofUQoDYkyubUKaB7bNtyMMSCCamSEYRutS2G2vm2w1ERShko8boRqdaaTAs4MR6sGYkTByNF"
}
```
Returns human-readable transaction information, parsed from the `getTransaction` method of the Solana RPC.
/getTokenAccounts { address }
Returns the token accounts owned by a user with an amount > 0. Derived from the `getTokenAccountsByOwner` method on the Solana RPC.
/getSignaturesForAddress { address }
Returns the transaction signatures returned in `getSignaturesForAddress` method from the Solana RPC.
/getTotalValue { address }
Returns the total value of the assets owned by `address`, broken down by NFTs and tokens. Token prices and NFT price estimates are provided by HelloMoon. An example output is provided below
```json
{
"total": "50.00",
"nftTotal": "25.00",
"tokenTotal": "25.00"
}
```
### Endpoints for NFT discovery
These endpoints are under development and subject to rapid change. These currently use the [Hyperspace API](https://docs.hyperspace.xyz).
/getCollectionsByFloorPrice { maxFloorPrice, minFloorPrice, orderBy, pageSize }
Returns
```json
{
"projects": [
{
"id": "",
"desc": "collection description",
"img": "collection image url",
"website": "collection website url",
"floor_price": 0.1
}
],
"hasMore": true,
"currentPage'": 1
}
```
/getListedCollectionNFTs { projectId, pageSize, priceOrder }
Returns LLM friendly response of available NFTs:
```json
{
"listings": [
{
"price": 0.1,
"token": "",
"marketplace": ""
}
],
"hasMore": true,
"currentPage": 1
}
```
## Private endpoints (not LLM accessible)
### Endpoints for Sending Transactions
Note: these endpoints are currently disabled in the production version of the ChatGPT plugin
/createBuyTransaction { token, price }
Right now we are trusting Hyperspace to craft a valid transaction for us.
In the future we will setup a write interface for programs on Solana to adhere to in order to
be a target of LLM transaction composition.
Returns
```json
{
"linkToSign": ""
}
```
/createTransferSol { destination, amount }
Creates a transaction to transfer an amount in Sol.
Returns
```json
{
"linkToSign": ""
}
```
/createTransferToken { destination, mint, amount }
Creates a transaction to transfer an amount of token (from the mint).
Returns
```json
{
"linkToSign": ""
}
```
### Endpoints for Transaction Composition
These are also subject to change, and we may create actual webpages to inspect
the transaction before signing. However for now, these are simply redirect links
to ensure that SolanaPay QR codes show up in the ChatGPT link previews.
/page/:methodName
Returns a webpage with [OpenGraph](https://ogp.me/) metadata that will be rendered in the ChatGPT
rich link preview. All ChatGPT links should be proxied through this sort of pipeline to maximize
user engagement of links. The `og:image` tag is to `/qr/:methodName` to show a SolanaPay QR code in link previews.
This is currently a blank page, but we may show a preview of the transaction in the future.
/qr/:methodName
Returns a PNG QR code that has been optimized to show in the particular aspect ratio of ChatGPT plugins.
This just encodes a SolanaPay link that redirects to `/sign/:methodName`.
/sign/:methodName
This is the final redirect link that actually returns transaction bytes in a SolanaPay compatible format
so users can sign transactions that are recommended by ChatGPT.
```json
{
"transaction": ""
}
```
## Development
To install dependencies, just execute `yarn`. This project uses `node` with version `>=16.17.0`.
To start a development server, execute `yarn dev`. This will start the plugin available from `localhost:3333` with its own configuration settings in `.well-known-dev/`.
# License
This codebase is released under [Apache License 2.0](LICENSE.md).
# Disclaimer
By accessing or using this codebase or any of its components, you accept and agree with the [Disclaimer](DISCLAIMER.md).
================================================
FILE: chatgpt-plugin/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
================================================
FILE: chatgpt-plugin/.prettierrc
================================================
{
"tabWidth": 2,
"useTabs": false,
"singleQuote": false,
"printWidth": 100,
"trailingComma": "all",
"arrowParens": "avoid",
"endOfLine": "auto",
"proseWrap": "always"
}
================================================
FILE: chatgpt-plugin/README.md
================================================
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
================================================
FILE: chatgpt-plugin/next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
// temporary until Sidekick has plugin autoselect
{
source: "/api/helloMoon/defi/tokenName",
destination: "/api/handlers/tokenName",
},
];
},
};
module.exports = nextConfig;
================================================
FILE: chatgpt-plugin/postcss.config.js
================================================
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
================================================
FILE: chatgpt-plugin/public/openapi.yaml
================================================
openapi: 3.0.2
info:
title: Solana Labs API
description: An API for retrieving human-readable information about the Solana blockchain.
version: 1.0.0
paths:
/api/handlers/getAssetsByOwner:
post:
summary: getAssetsByOwner
description: Accepts Solana publicKey address. Returns Metaplex NFTs owned by the address
operationId: query_assets_by_owner
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/getAccountInfoRequest"
required: true
responses:
"200":
description: Successful Response
"500":
description: Validation Error
security:
- HTTPBearer: []
/api/handlers/getAccountInfo:
post:
summary: getAccountInfo
description:
Returns information about the data stored by that account in a human-readable format.
Human-readable formatting is only possible when the account's corresponding program owner
has published an Anchor IDL on the Solana blockchain.
operationId: query_account_info
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/getAccountInfoRequest"
required: true
responses:
"200":
description: Successful Response
"500":
description: Validation Error
security:
- HTTPBearer: []
/api/handlers/getTokenAccounts:
post:
summary: getTokenAccounts
description:
Returns the fungible and non-fungible tokens and amounts owned by the address. May show
tokens not listed in get_assets_by_owner.
operationId: query_token_accounts
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/getBalanceRequest"
required: true
responses:
"200":
description: Successful Response
"500":
description: Validation Error
security:
- HTTPBearer: []
/api/handlers/getBalance:
post:
summary: getBalance
description:
Accepts Solana publicKey address. Returns the amount of lamports that the account has
available.
operationId: query_balance
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/getBalanceRequest"
required: true
responses:
"200":
description: Successful Response
"500":
description: Validation Error
security:
- HTTPBearer: []
/api/handlers/getTransaction:
post:
summary: getTransaction
description:
Accepts a transaction signature. Returns the publicly available transaction information and
metadata. Only high level summaries based on instruction data should be provided to users
unless otherwise specified. Logs, compute units, and fees are available in devMode.
operationId: query_transaction
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/getTransactionRequest"
responses:
"200":
description: Successful Response
"500":
description: Validation Error
security:
- HTTPBearer: []
/api/handlers/getSignaturesForAddress:
post:
summary: getSignaturesForAddress
description:
Accepts Solana publicKey address. Returns the latest transaction signatures that involve
that address.
operationId: query_signatures_for_address
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/getSignaturesForAddressRequest"
required: true
responses:
"200":
description: Successful Response
"500":
description: Validation Error
security:
- HTTPBearer: []
/api/handlers/getCollectionsByFloorPrice:
post:
summary: Search through Solana NFT collections by floor price
operationId: query_nft_collections_by_fp
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/getCollectionsByFloorPriceRequest"
responses:
"200":
description: Successful Response
"500":
description: Validation Error
security:
- HTTPBearer: []
/api/handlers/getListedCollectionNFTs:
post:
summary: Returns the listed NFTs in a collection available to purchase
operationId: query_listed_nfts_for_collection
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/getListedCollectionNFTsRequest"
responses:
"200":
description: Successful Response
"500":
description: Validation Error
security:
- HTTPBearer: []
/api/handlers/getCollectionsByName:
post:
summary: Searches for NFT collections based on project name
operationId: query_nfts_collections_by_name
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/getCollectionsByNameRequest"
responses:
"200":
description: Successful Response
"500":
description: Validation Error
security:
- HTTPBearer: []
/api/handlers/getTotalValue:
post:
summary:
Calculates the total value of a the address's holdings in USD, with breakdown by NFTs and
tokens.
operationId: query_total_value
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/getTotalValueRequest"
responses:
"200":
description: Successful Response
"500":
description: Validation Error
security:
- HTTPBearer: []
/api/handlers/walletName:
post:
summary:
Resolves wallet names to the actual solana address, or if it is already a Solana address, it
looks up all the wallet names associated with that publickey. This works especially well for
.sol, .glow, .backpack, or .poor domains.
operationId: query_wallet_name
requestBody:
content:
application/json:
schema:
type: object
properties:
walletName:
type: string
responses:
"200":
description: Successful Response
"500":
description: Validation Error
security:
- HTTPBearer: []
/api/handlers/tokenName:
post:
summary:
Searches tokens by name, and returns the mint address, logo, name and symbol for up to 10
relevant, verified tokens.
operationId: search_token_name
requestBody:
content:
application/json:
schema:
type: object
properties:
tokenName:
type: string
responses:
"200":
description: Successful Response
"500":
description: Validation Error
security:
- HTTPBearer: []
components:
schemas:
getAccountInfoRequest:
type: object
required:
- address
properties:
address:
type: string
description: Base58 encoded PublicKey
getSignaturesForAddressRequest:
title: GetSignaturesForAddressRequest
type: object
required:
- address
properties:
address:
type: string
description: Base58 encoded PublicKey
beforeSignature:
type: string
untilSignature:
type: string
getBalanceRequest:
title: GetBalanceRequest
type: object
required:
- address
properties:
address:
title: Address
type: string
description: Base58 encoded PublicKey
getTransactionRequest:
type: object
required:
- signature
properties:
signature:
type: string
devMode:
type: boolean
getCollectionsByFloorPriceRequest:
type: object
properties:
maxFloorPrice:
type: number
nullable: true
minFloorPrice:
type: number
nullable: true
orderBy:
type: string
enum:
- ASC
- DESC
nullable: true
getCollectionsByNameRequest:
type: object
properties:
projectName:
type: string
getListedCollectionNFTsRequest:
type: object
properties:
projectId:
type: string
priceOrder:
type: string
nullable: true
pageSize:
type: number
nullable: true
getTotalValueRequest:
type: object
properties:
address:
type: string
description: Base58 encoded PublicKey
securitySchemes:
HTTPBearer:
type: http
scheme: bearer
================================================
FILE: chatgpt-plugin/src/app/globals.css
================================================
@tailwind base;
@tailwind components;
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb)))
rgb(var(--background-start-rgb));
}
.ms-6 {
margin-left: 2rem !important;
}
img,
svg {
vertical-align: middle;
}
@media (min-width: 768px) {
.AiHero_hero__title__ZatKJ {
font-size: 6rem;
line-height: 6rem;
margin-bottom: 32px !important;
}
}
.AiHero_hero__title__ZatKJ {
font-weight: 700;
font-size: 3.5rem;
margin-bottom: 24px !important;
}
h2 {
font-size: 2.425rem;
line-height: 3.15rem;
font-weight: 400;
}
.czAcYK.btn {
font-family: DSemi, monospace;
font-weight: normal;
text-transform: uppercase;
line-height: 1.25rem;
font-size: 0.95rem;
width: fit-content;
background-color: rgb(153, 69, 255);
border-radius: 2.25rem;
padding: 0.875rem 1.5rem;
color: white;
border: 1px solid transparent;
}
.lift {
transition: box-shadow 0.25s ease, transform 0.25s ease;
}
.btn {
background-color: rgb(153, 69, 255);
@apply flex items-center gap-3 px-5 py-3 text-lg transition transform whitespace-nowrap rounded-3xl;
@apply hover:bg-white hover:text-black;
@apply hover:-translate-y-1;
}
.btn svg {
@apply w-5 h-5;
}
.disclosure {
@apply flex items-center justify-between w-full px-4 py-2 rounded-xl;
@apply text-white bg-gray-800;
@apply hover:bg-opacity-80;
@apply border border-gray-700;
}
.disclosure-open {
@apply font-semibold;
@apply bg-gray-800 border-gray-600 hover:bg-opacity-100;
}
.disclosure svg {
@apply w-5 h-5;
}
/* utilities at the end to make the inline classes a priority */
@tailwind utilities;
.legal {
text-decoration: underline;
color: #00ffbd;
}
================================================
FILE: chatgpt-plugin/src/app/layout.tsx
================================================
import Link from "next/link";
import "./globals.css";
import { Inter } from "next/font/google";
import Image from "next/image";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "Solana Labs - ChatGPT Plugin",
description: "Get a taste of Solana",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
================================================
FILE: chatgpt-plugin/src/app/page.tsx
================================================
"use client";
import Head from "next/head";
import Link from "next/link";
import { features } from "@/lib/features";
import { Disclosure } from "@headlessui/react";
import { ArrowRightIcon, ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline";
export default function Home() {
return (
<>
Solana Labs - ChatGPT Plugin
Solana Labs
ChatGPT Plugin
The Solana Labs ChatGPT Plugin helps you query information on the Solana blockchain. Solana Labs has provided the code as a reference implementation with permissive
licensing on Github. A list of features and example queries is provided below.
GitHub Repo
AI Grants
Features
{features.map((endpoint, id) => (
{({ open }) => (
{`Example: "What data is in `}
ACCOUNT-ADDRESS
{` ?"`}
{`Returns data about the account, including the owner, the amount of Sol, and the potentially human-readable data stored in the account, if it's owner program has an Anchor IDL.`}
{`Example: "What happened in transaction `}
TRANSACTION-ID
{` ?"`}
{`Returns information about the transaction, and potentially human readable information about program instructions, if the programs have Anchor IDLs.`}
),
},
{
label: "Find NFTs for a wallet",
description: (
{`Example: "What NFTs does `}
MY-WALLET-ADDRESS
{` own?"`}
{`The plugin can list your NFTs and their metadata. If you have a lot of NFTs, you can ask it to list all of your NFTs 5 at a time.`}
{`Example: "What are the latest transactions for `}
WALLET-ADDRESS
{` ?"`}
{`The plugin can help you go through wallet transactions and explore on-chain activity.`}
),
},
{
label: "Search NFT collections by name or floor price",
description: (
{`Example: "Can you find me the cheapest Mad Lads NFT for sale?"`}
{`NFTs are searched by name and sorted by floor price, you can ask the model to search explicitly by either target floor price name or by name.
NFTs within a collection can also be sorted by floor price.`}
Note:
{` If you cannot find your NFT Collection, please see if it exists on `}
hyperspace.xyz
{`. We
use Hyperspace's API to support this plugin`}
{`Example: "What is the value of `}
MY-ADDRESS
{` ?"`}
{`Wallet value is calculated by estimating the value of all tokens and NFTs owned by the wallet. Please note that these values are in USD and are subject to change due to market fluctuations. Price estimates are provided by `}
{`HelloMoon's`}
{` API.`}
// {`Example: "Can you help me transfer 1 EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v to `}
// DESTINATION
// {` ?"`}
//
//
// This will generate a QR code that you can scan from within your mobile wallet to send 1 USDC
// (mint address EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v) from your wallet to a
// destination wallet. See the{" "}
//
// Disclaimer
//
// {" and "}
//
// Terms of Service
//
// {"."}
//
// {`Example: "Can you help me transfer 1 SOL to `}
// DESTINATION
// {` ?"`}
//
//
// This will generate a QR code that you can scan from within your mobile wallet to send 1 Sol
// from your wallet to a destination wallet address. See the{" "}
//
// Disclaimer
//
// {" and "}
//
// Terms of Service
//
// {"."}
//
// {`Example: "Can you help me transfer 1 EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v to `}
// DESTINATION
// {` ?"`}
//
//
// This will generate a QR code that you can scan from within your mobile wallet to send 1 USDC
// (mint address EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v) from your wallet to a
// destination wallet. See the{" "}
//
// Disclaimer
//
// {" and "}
//
// Terms of Service
//
// {"."}
//
// ),
// },
];
================================================
FILE: chatgpt-plugin/src/lib/helloMoon/index.ts
================================================
export const VALID_OPERATORS = ["=", "!=", ">", "<", ">="];
export type Comparison =
| {
operator: "=" | "!=" | ">" | "<" | ">=";
value: number;
}
| undefined;
export function buildComparison(
operator: string | undefined,
value: number | undefined,
): Comparison {
if (operator && !VALID_OPERATORS.includes(operator)) {
throw new Error(
"Invalid operator: " +
operator +
". Valid operators are: " +
VALID_OPERATORS.join(", ") +
".",
);
}
const comparison =
operator && value !== undefined
? {
operator,
value: new Number(value).valueOf(),
}
: undefined;
return comparison as Comparison;
}
/**
* Rewrites a "B - A" pair to "A-B" pair
* @param pair
*/
export function cleanSwapPair(pair: string) {
let cleaned = pair.replaceAll(" ", "");
let split = cleaned.split("-");
let pairA = split[0];
let pairB = split[1];
if (pairA < pairB) {
return `${pairA} - ${pairB}`;
}
return `${pairB} - ${pairA}`;
}
================================================
FILE: chatgpt-plugin/src/lib/hyperspace/account.ts
================================================
import { Keypair, PublicKey } from "@solana/web3.js";
import * as anchor from "@coral-xyz/anchor";
import {
HYPERSPACE,
FEE_PAYER,
HYPERSPACE_ID as MARKETPLACE_PROGRAM_ID,
} from "./constants";
import { hyperspaceIdl } from "./idl/hyperspace";
import { AnchorProvider } from "@coral-xyz/anchor";
import {
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID as SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,
} from "@solana/spl-token";
import { PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID } from "@metaplex-foundation/mpl-token-metadata";
export async function loadHyperspaceProgram(
anchorWallet: any,
connection: anchor.web3.Connection
) {
const provider = new AnchorProvider(connection, anchorWallet, {
preflightCommitment: "recent",
});
return new anchor.Program(hyperspaceIdl, MARKETPLACE_PROGRAM_ID, provider);
}
export const getHyperspaceBuyerEscrow = async (
auctionHouse: anchor.web3.PublicKey,
wallet: anchor.web3.PublicKey
): Promise<[PublicKey, number]> => {
return await anchor.web3.PublicKey.findProgramAddress(
[Buffer.from(HYPERSPACE), auctionHouse.toBuffer(), wallet.toBuffer()],
MARKETPLACE_PROGRAM_ID
);
};
export const getHyperspaceTradeState = async (
is_buy: boolean,
wallet: anchor.web3.PublicKey,
tokenAccount: anchor.web3.PublicKey,
tokenMint: anchor.web3.PublicKey,
tokenSize: anchor.BN
): Promise<[PublicKey, number]> => {
return await anchor.web3.PublicKey.findProgramAddress(
[
Buffer.from(HYPERSPACE),
is_buy
? new anchor.BN(1).toArrayLike(Buffer, "le", 1)
: new anchor.BN(0).toArrayLike(Buffer, "le", 1),
wallet.toBuffer(),
tokenAccount.toBuffer(),
tokenMint.toBuffer(),
tokenSize.toArrayLike(Buffer, "le", 8),
],
MARKETPLACE_PROGRAM_ID
);
};
export const getMetadata = async (
mint: anchor.web3.PublicKey
): Promise => {
return (
await anchor.web3.PublicKey.findProgramAddress(
[
Buffer.from("metadata"),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
mint.toBuffer(),
],
TOKEN_METADATA_PROGRAM_ID
)
)[0];
};
export const getHyperspace = async (
creator: anchor.web3.PublicKey
): Promise<[PublicKey, number]> => {
return await anchor.web3.PublicKey.findProgramAddress(
[Buffer.from(HYPERSPACE), creator.toBuffer()],
MARKETPLACE_PROGRAM_ID
);
};
export const getHyperspaceFeeAcct = async (
auctionHouse: anchor.web3.PublicKey
): Promise<[PublicKey, number]> => {
return await anchor.web3.PublicKey.findProgramAddress(
[Buffer.from(HYPERSPACE), auctionHouse.toBuffer(), Buffer.from(FEE_PAYER)],
MARKETPLACE_PROGRAM_ID
);
};
export const getAtaForMint = async (
mint: anchor.web3.PublicKey,
buyer: anchor.web3.PublicKey
): Promise<[anchor.web3.PublicKey, number]> => {
return await anchor.web3.PublicKey.findProgramAddress(
[buyer.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()],
SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID
);
};
export const getEditionDataAccount = async (
mint: anchor.web3.PublicKey
): Promise<[anchor.web3.PublicKey, number]> => {
return await anchor.web3.PublicKey.findProgramAddress(
[
Buffer.from("metadata"),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
mint.toBuffer(),
Buffer.from("edition"),
],
TOKEN_METADATA_PROGRAM_ID
);
};
export const getHyperspaceProgramAsSigner = async (): Promise<
[PublicKey, number]
> => {
return await anchor.web3.PublicKey.findProgramAddress(
[Buffer.from(HYPERSPACE), Buffer.from("signer")],
MARKETPLACE_PROGRAM_ID
);
};
export const findTokenRecordPda = (
mint: PublicKey,
token: PublicKey
): PublicKey => {
return PublicKey.findProgramAddressSync(
[
Buffer.from("metadata"),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
mint.toBuffer(),
Buffer.from("token_record"),
token.toBuffer(),
],
TOKEN_METADATA_PROGRAM_ID
)[0];
};
================================================
FILE: chatgpt-plugin/src/lib/hyperspace/constants.ts
================================================
import { PublicKey } from "@solana/web3.js";
export const FEE_PAYER = "fee_payer";
export const HYPERSPACE = "hyperspace";
export const HYPERSPACE_ID = new PublicKey(
"HYPERfwdTjyJ2SCaKHmpF2MtrXqWxrsotYDsTrshHWq8"
);
export const HYPERSPACE_MARKETPLACE_INSTANCE_ID_STRING =
"5pdaXth4ijgDCeYDKgSx3jAbN7m8h4gy1LRCErAAN1LM";
export const HYPERSPACE_MARKETPLACE_INSTANCE = new PublicKey(
"5pdaXth4ijgDCeYDKgSx3jAbN7m8h4gy1LRCErAAN1LM"
);
================================================
FILE: chatgpt-plugin/src/lib/hyperspace/idl/hyperspace.ts
================================================
export type Hyperspace = {
version: "0.1.0";
name: "hyperspace";
instructions: [
{
name: "initProgramAsSigner";
accounts: [
{
name: "wallet";
isMut: true;
isSigner: true;
},
{
name: "programAsSigner";
isMut: true;
isSigner: false;
},
{
name: "systemProgram";
isMut: false;
isSigner: false;
}
];
args: [
{
name: "programAsSignerBump";
type: "u8";
}
];
},
{
name: "updateHyperspace";
accounts: [
{
name: "payer";
isMut: false;
isSigner: true;
},
{
name: "authority";
isMut: false;
isSigner: true;
},
{
name: "newAuthority";
isMut: false;
isSigner: false;
},
{
name: "feeWithdrawalDestination";
isMut: true;
isSigner: false;
},
{
name: "treasuryWithdrawalDestination";
isMut: true;
isSigner: false;
},
{
name: "treasuryWithdrawalDestinationOwner";
isMut: false;
isSigner: false;
},
{
name: "hyperspace";
isMut: true;
isSigner: false;
},
{
name: "tokenProgram";
isMut: false;
isSigner: false;
},
{
name: "systemProgram";
isMut: false;
isSigner: false;
},
{
name: "ataProgram";
isMut: false;
isSigner: false;
},
{
name: "rent";
isMut: false;
isSigner: false;
}
];
args: [
{
name: "sellerFeeBasisPoints";
type: {
option: "u16";
};
},
{
name: "requiresSignOff";
type: {
option: "bool";
};
},
{
name: "canChangeSalePrice";
type: {
option: "bool";
};
}
];
},
{
name: "withdraw";
accounts: [
{
name: "wallet";
isMut: false;
isSigner: false;
},
{
name: "receiptAccount";
isMut: true;
isSigner: false;
},
{
name: "escrowPaymentAccount";
isMut: true;
isSigner: false;
},
{
name: "authority";
isMut: false;
isSigner: false;
},
{
name: "hyperspace";
isMut: false;
isSigner: false;
},
{
name: "tokenProgram";
isMut: false;
isSigner: false;
},
{
name: "systemProgram";
isMut: false;
isSigner: false;
},
{
name: "rent";
isMut: false;
isSigner: false;
}
];
args: [
{
name: "escrowPaymentBump";
type: "u8";
},
{
name: "amount";
type: "u64";
}
];
},
{
name: "deposit";
accounts: [
{
name: "wallet";
isMut: false;
isSigner: true;
},
{
name: "paymentAccount";
isMut: true;
isSigner: false;
},
{
name: "transferAuthority";
isMut: false;
isSigner: false;
},
{
name: "escrowPaymentAccount";
isMut: true;
isSigner: false;
},
{
name: "authority";
isMut: false;
isSigner: false;
},
{
name: "hyperspace";
isMut: false;
isSigner: false;
},
{
name: "hyperspaceFeeAccount";
isMut: true;
isSigner: false;
},
{
name: "tokenProgram";
isMut: false;
isSigner: false;
},
{
name: "systemProgram";
isMut: false;
isSigner: false;
},
{
name: "rent";
isMut: false;
isSigner: false;
}
];
args: [
{
name: "escrowPaymentBump";
type: "u8";
},
{
name: "amount";
type: "u64";
}
];
},
{
name: "permissionlessCancel";
accounts: [
{
name: "wallet";
isMut: false;
isSigner: true;
},
{
name: "tokenMint";
isMut: false;
isSigner: false;
},
{
name: "seller";
isMut: false;
isSigner: false;
},
{
name: "tokenAccount";
isMut: false;
isSigner: false;
},
{
name: "tradeState";
isMut: true;
isSigner: false;
},
{
name: "programAsSigner";
isMut: false;
isSigner: false;
}
];
args: [
{
name: "programAsSignerBump";
type: "u8";
},
{
name: "tradeStateBump";
type: "u8";
},
{
name: "tokenSize";
type: "u64";
}
];
},
{
name: "collectionCancelBuy";
accounts: [
{
name: "wallet";
isMut: true;
isSigner: false;
},
{
name: "identifierPubkey";
isMut: false;
isSigner: false;
},
{
name: "authority";
isMut: true;
isSigner: false;
},
{
name: "hyperspace";
isMut: false;
isSigner: false;
},
{
name: "collectionBuyerTradeState";
isMut: true;
isSigner: false;
},
{
name: "programAsSigner";
isMut: false;
isSigner: false;
},
{
name: "tokenProgram";
isMut: false;
isSigner: false;
}
];
args: [
{
name: "buyerPrice";
type: "u64";
},
{
name: "programAsSignerBump";
type: "u8";
},
{
name: "tradeStateBump";
type: "u8";
},
{
name: "collectionTradeStateType";
type: "u8";
},
{
name: "identifierIndex";
type: "u8";
}
];
},
{
name: "cancel";
accounts: [
{
name: "wallet";
isMut: true;
isSigner: false;
docs: ["CHECK OWNER OF TRADE_STATE TODO"];
},
{
name: "tokenMint";
isMut: false;
isSigner: false;
},
{
name: "tokenAccount";
isMut: true;
isSigner: false;
},
{
name: "metadata";
isMut: true;
isSigner: false;
},
{
name: "authority";
isMut: false;
isSigner: false;
},
{
name: "hyperspace";
isMut: false;
isSigner: false;
},
{
name: "tradeState";
isMut: true;
isSigner: false;
},
{
name: "tokenProgram";
isMut: false;
isSigner: false;
},
{
name: "systemProgram";
isMut: false;
isSigner: false;
},
{
name: "metadataProgram";
isMut: false;
isSigner: false;
},
{
name: "programAsSigner";
isMut: false;
isSigner: false;
},
{
name: "instructions";
isMut: false;
isSigner: false;
},
{
name: "tokenRecord";
isMut: true;
isSigner: false;
docs: ["CHECK no check needed"];
},
{
name: "editionAccount";
isMut: false;
isSigner: false;
docs: ["CHECK no check needed"];
},
{
name: "authorizationRules";
isMut: false;
isSigner: false;
docs: ["CHECK no check needed"];
},
{
name: "mplTokenAuthRulesProgram";
isMut: false;
isSigner: false;
docs: ["CHECK no check needed"];
}
];
args: [
{
name: "isBuy";
type: "u8";
},
{
name: "programAsSignerBump";
type: "u8";
},
{
name: "tradeStateBump";
type: "u8";
},
{
name: "tokenSize";
type: "u64";
}
];
},
{
name: "collectionExecuteSale";
accounts: [
{
name: "buyer";
isMut: true;
isSigner: false;
},
{
name: "buyerBrokerWallet";
isMut: true;
isSigner: false;
},
{
name: "seller";
isMut: true;
isSigner: false;
},
{
name: "sellerBrokerWallet";
isMut: true;
isSigner: false;
},
{
name: "tokenAccount";
isMut: true;
isSigner: false;
},
{
name: "tokenMint";
isMut: false;
isSigner: false;
},
{
name: "metadata";
isMut: true;
isSigner: false;
},
{
name: "editionData";
isMut: false;
isSigner: false;
},
{
name: "escrowPaymentAccount";
isMut: true;
isSigner: false;
},
{
name: "buyerReceiptTokenAccount";
isMut: true;
isSigner: false;
},
{
name: "authority";
isMut: false;
isSigner: false;
},
{
name: "hyperspace";
isMut: false;
isSigner: false;
},
{
name: "hyperspaceTreasury";
isMut: true;
isSigner: false;
},
{
name: "identifierPubkey";
isMut: false;
isSigner: false;
},
{
name: "collectionBuyerTradeState";
isMut: true;
isSigner: false;
},
{
name: "sellerTradeState";
isMut: true;
isSigner: false;
},
{
name: "tokenProgram";
isMut: false;
isSigner: false;
},
{
name: "systemProgram";
isMut: false;
isSigner: false;
},
{
name: "ataProgram";
isMut: false;
isSigner: false;
},
{
name: "metadataProgram";
isMut: false;
isSigner: false;
},
{
name: "mplTokenAuthRulesProgram";
isMut: false;
isSigner: false;
},
{
name: "programAsSigner";
isMut: false;
isSigner: false;
},
{
name: "rent";
isMut: false;
isSigner: false;
},
{
name: "instructions";
isMut: false;
isSigner: false;
},
{
name: "ownerTokenRecord";
isMut: true;
isSigner: false;
docs: ["CHECK no check needed"];
},
{
name: "destinationTokenRecord";
isMut: true;
isSigner: false;
docs: ["CHECK no check needed"];
},
{
name: "authorizationRules";
isMut: false;
isSigner: false;
docs: ["CHECK no check needed"];
}
];
args: [
{
name: "escrowPaymentBump";
type: "u8";
},
{
name: "programAsSignerBump";
type: "u8";
},
{
name: "buyerTradeStateBump";
type: "u8";
},
{
name: "sellerTradeStateBump";
type: "u8";
},
{
name: "maxAmountToPay";
type: "u64";
},
{
name: "buyerBrokerBasisPoints";
type: "u16";
},
{
name: "sellerBrokerBasisPoints";
type: "u16";
},
{
name: "tokenSize";
type: "u64";
},
{
name: "collectionTradeStateType";
type: "u8";
},
{
name: "identifierIndex";
type: "u8";
},
{
name: "royaltyBasisPoints";
type: "u16";
}
];
},
{
name: "executeSale";
accounts: [
{
name: "buyer";
isMut: true;
isSigner: false;
},
{
name: "buyerBrokerWallet";
isMut: true;
isSigner: false;
},
{
name: "seller";
isMut: true;
isSigner: false;
},
{
name: "sellerBrokerWallet";
isMut: true;
isSigner: false;
},
{
name: "tokenAccount";
isMut: true;
isSigner: false;
},
{
name: "tokenMint";
isMut: false;
isSigner: false;
},
{
name: "metadata";
isMut: true;
isSigner: false;
},
{
name: "escrowPaymentAccount";
isMut: true;
isSigner: false;
},
{
name: "buyerReceiptTokenAccount";
isMut: true;
isSigner: false;
},
{
name: "authority";
isMut: false;
isSigner: false;
},
{
name: "hyperspace";
isMut: false;
isSigner: false;
},
{
name: "hyperspaceTreasury";
isMut: true;
isSigner: false;
},
{
name: "buyerTradeState";
isMut: true;
isSigner: false;
},
{
name: "sellerTradeState";
isMut: true;
isSigner: false;
},
{
name: "tokenProgram";
isMut: false;
isSigner: false;
},
{
name: "systemProgram";
isMut: false;
isSigner: false;
},
{
name: "ataProgram";
isMut: false;
isSigner: false;
},
{
name: "metadataProgram";
isMut: false;
isSigner: false;
},
{
name: "mplTokenAuthRulesProgram";
isMut: false;
isSigner: false;
},
{
name: "programAsSigner";
isMut: false;
isSigner: false;
},
{
name: "rent";
isMut: false;
isSigner: false;
},
{
name: "instructions";
isMut: false;
isSigner: false;
},
{
name: "editionAccount";
isMut: false;
isSigner: false;
docs: ["CHECK no check needed"];
},
{
name: "ownerTokenRecord";
isMut: true;
isSigner: false;
docs: ["CHECK no check needed"];
},
{
name: "destinationTokenRecord";
isMut: true;
isSigner: false;
docs: ["CHECK no check needed"];
},
{
name: "authorizationRules";
isMut: false;
isSigner: false;
docs: ["CHECK no check needed"];
}
];
args: [
{
name: "escrowPaymentBump";
type: "u8";
},
{
name: "programAsSignerBump";
type: "u8";
},
{
name: "buyerTradeStateBump";
type: "u8";
},
{
name: "sellerTradeStateBump";
type: "u8";
},
{
name: "maxAmountToPay";
type: "u64";
},
{
name: "buyerBrokerBasisPoints";
type: "u16";
},
{
name: "sellerBrokerBasisPoints";
type: "u16";
},
{
name: "tokenSize";
type: "u64";
},
{
name: "royaltyBasisPoints";
type: "u16";
}
];
},
{
name: "sell";
accounts: [
{
name: "wallet";
isMut: false;
isSigner: true;
},
{
name: "sellerBrokerWallet";
isMut: false;
isSigner: false;
},
{
name: "tokenMint";
isMut: false;
isSigner: false;
},
{
name: "tokenAccount";
isMut: true;
isSigner: false;
},
{
name: "metadata";
isMut: true;
isSigner: false;
},
{
name: "authority";
isMut: false;
isSigner: false;
},
{
name: "hyperspace";
isMut: false;
isSigner: false;
},
{
name: "hyperspaceFeeAccount";
isMut: true;
isSigner: false;
},
{
name: "sellerTradeState";
isMut: true;
isSigner: false;
},
{
name: "tokenProgram";
isMut: false;
isSigner: false;
},
{
name: "systemProgram";
isMut: false;
isSigner: false;
},
{
name: "metadataProgram";
isMut: false;
isSigner: false;
},
{
name: "programAsSigner";
isMut: false;
isSigner: false;
},
{
name: "instructions";
isMut: false;
isSigner: false;
},
{
name: "tokenRecord";
isMut: true;
isSigner: false;
docs: ["CHECK no check needed"];
},
{
name: "editionAccount";
isMut: false;
isSigner: false;
docs: ["CHECK no check needed"];
},
{
name: "authorizationRules";
isMut: false;
isSigner: false;
docs: ["CHECK no check needed"];
},
{
name: "mplTokenAuthRulesProgram";
isMut: false;
isSigner: false;
docs: ["CHECK no check needed"];
},
{
name: "clock";
isMut: false;
isSigner: false;
}
];
args: [
{
name: "tradeStateBump";
type: "u8";
},
{
name: "programAsSignerBump";
type: "u8";
},
{
name: "minAmountToReceive";
type: "u64";
},
{
name: "brokerBasisPoints";
type: "u16";
},
{
name: "tokenSize";
type: "u64";
},
{
name: "royaltyBasisPoints";
type: "u16";
}
];
},
{
name: "buy";
accounts: [
{
name: "wallet";
isMut: false;
isSigner: true;
},
{
name: "buyerBrokerWallet";
isMut: false;
isSigner: false;
},
{
name: "paymentAccount";
isMut: true;
isSigner: false;
},
{
name: "transferAuthority";
isMut: false;
isSigner: false;
},
{
name: "tokenAccount";
isMut: false;
isSigner: false;
},
{
name: "buyerReceiptTokenAccount";
isMut: false;
isSigner: false;
},
{
name: "metadata";
isMut: false;
isSigner: false;
},
{
name: "escrowPaymentAccount";
isMut: true;
isSigner: false;
},
{
name: "authority";
isMut: false;
isSigner: false;
},
{
name: "hyperspace";
isMut: false;
isSigner: false;
},
{
name: "hyperspaceFeeAccount";
isMut: true;
isSigner: false;
},
{
name: "buyerTradeState";
isMut: true;
isSigner: false;
},
{
name: "tokenProgram";
isMut: false;
isSigner: false;
},
{
name: "systemProgram";
isMut: false;
isSigner: false;
},
{
name: "rent";
isMut: false;
isSigner: false;
},
{
name: "clock";
isMut: false;
isSigner: false;
}
];
args: [
{
name: "tradeStateBump";
type: "u8";
},
{
name: "escrowPaymentBump";
type: "u8";
},
{
name: "maxAmountToPay";
type: "u64";
},
{
name: "brokerBasisPoints";
type: "u16";
},
{
name: "tokenSize";
type: "u64";
},
{
name: "royaltyBasisPoints";
type: "u16";
}
];
},
{
name: "collectionBuy";
accounts: [
{
name: "wallet";
isMut: true;
isSigner: true;
},
{
name: "buyerBrokerWallet";
isMut: false;
isSigner: false;
},
{
name: "identifierPubkey";
isMut: false;
isSigner: false;
},
{
name: "escrowPaymentAccount";
isMut: true;
isSigner: false;
},
{
name: "authority";
isMut: false;
isSigner: false;
},
{
name: "hyperspace";
isMut: false;
isSigner: false;
},
{
name: "hyperspaceFeeAccount";
isMut: true;
isSigner: false;
},
{
name: "collectionBuyerTradeState";
isMut: true;
isSigner: false;
},
{
name: "tokenProgram";
isMut: false;
isSigner: false;
},
{
name: "systemProgram";
isMut: false;
isSigner: false;
},
{
name: "rent";
isMut: false;
isSigner: false;
},
{
name: "clock";
isMut: false;
isSigner: false;
}
];
args: [
{
name: "tradeStateBump";
type: "u8";
},
{
name: "escrowPaymentBump";
type: "u8";
},
{
name: "buyerPrice";
type: "u64";
},
{
name: "brokerBasisPoints";
type: "u16";
},
{
name: "collectionTradeStateType";
type: "u8";
},
{
name: "identifierIndex";
type: "u8";
},
{
name: "royaltyBasisPoints";
type: "u16";
}
];
},
{
name: "createCollectionBuyTradeState";
accounts: [
{
name: "wallet";
isMut: true;
isSigner: true;
},
{
name: "identifierPubkey";
isMut: false;
isSigner: false;
},
{
name: "brokerWallet";
isMut: false;
isSigner: false;
},
{
name: "tradeState";
isMut: true;
isSigner: false;
},
{
name: "systemProgram";
isMut: false;
isSigner: false;
},
{
name: "clock";
isMut: false;
isSigner: false;
}
];
args: [
{
name: "tradeStateBump";
type: "u8";
},
{
name: "buyPrice";
type: "u64";
},
{
name: "brokerBasisPoints";
type: "u16";
},
{
name: "collectionTradeStateType";
type: "u8";
},
{
name: "identifierIndex";
type: "u8";
},
{
name: "royaltyBasisPoints";
type: "u16";
}
];
},
{
name: "createTradeState";
accounts: [
{
name: "wallet";
isMut: true;
isSigner: true;
},
{
name: "collection";
isMut: false;
isSigner: false;
},
{
name: "brokerWallet";
isMut: false;
isSigner: false;
},
{
name: "tokenAccount";
isMut: false;
isSigner: false;
},
{
name: "tokenMint";
isMut: false;
isSigner: false;
},
{
name: "tradeState";
isMut: true;
isSigner: false;
},
{
name: "systemProgram";
isMut: false;
isSigner: false;
},
{
name: "clock";
isMut: false;
isSigner: false;
}
];
args: [
{
name: "isBuy";
type: "u8";
},
{
name: "tradeStateBump";
type: "u8";
},
{
name: "buyPrice";
type: "u64";
},
{
name: "brokerBasisPoints";
type: "u16";
},
{
name: "tokenSize";
type: "u64";
},
{
name: "royaltyBasisPoints";
type: "u16";
}
];
}
];
accounts: [
{
name: "hyperspace";
type: {
kind: "struct";
fields: [
{
name: "hyperspaceFeeAccount";
type: "publicKey";
},
{
name: "hyperspaceTreasury";
type: "publicKey";
},
{
name: "treasuryWithdrawalDestination";
type: "publicKey";
},
{
name: "feeWithdrawalDestination";
type: "publicKey";
},
{
name: "authority";
type: "publicKey";
},
{
name: "creator";
type: "publicKey";
},
{
name: "bump";
type: "u8";
},
{
name: "treasuryBump";
type: "u8";
},
{
name: "feePayerBump";
type: "u8";
},
{
name: "sellerFeeBasisPoints";
type: "u16";
},
{
name: "requiresSignOff";
type: "bool";
},
{
name: "canChangeSalePrice";
type: "bool";
}
];
};
},
{
name: "programAsSigner";
type: {
kind: "struct";
fields: [
{
name: "firstByte";
type: "u8";
}
];
};
},
{
name: "tradeState";
type: {
kind: "struct";
fields: [
{
name: "buyPrice";
type: "u64";
},
{
name: "userWallet";
type: "publicKey";
},
{
name: "isBuy";
type: "u8";
},
{
name: "collection";
type: "publicKey";
},
{
name: "brokerWallet";
type: "publicKey";
},
{
name: "brokerBasisPoints";
type: "u16";
},
{
name: "tokenMint";
type: "publicKey";
},
{
name: "royaltyBasisPoints";
type: "u16";
},
{
name: "timestamp";
type: "i64";
}
];
};
},
{
name: "collectionBuyTradeState";
type: {
kind: "struct";
fields: [
{
name: "userWallet";
type: "publicKey";
},
{
name: "collectionTradeStateType";
type: "u8";
},
{
name: "identifierIndex";
type: "u8";
},
{
name: "identifierPubkey";
type: "publicKey";
},
{
name: "brokerWallet";
type: "publicKey";
},
{
name: "brokerBasisPoints";
type: "u16";
},
{
name: "royaltyBasisPoints";
type: "u16";
},
{
name: "timestamp";
type: "i64";
}
];
};
}
];
types: [
{
name: "AuthorizationDataLocal";
type: {
kind: "struct";
fields: [
{
name: "payload";
type: {
vec: {
defined: "TaggedPayload";
};
};
}
];
};
},
{
name: "TaggedPayload";
type: {
kind: "struct";
fields: [
{
name: "name";
type: "string";
},
{
name: "payload";
type: {
defined: "PayloadTypeLocal";
};
}
];
};
},
{
name: "SeedsVecLocal";
type: {
kind: "struct";
fields: [
{
name: "seeds";
docs: ["The vector of derivation seeds."];
type: {
vec: "bytes";
};
}
];
};
},
{
name: "ProofInfoLocal";
type: {
kind: "struct";
fields: [
{
name: "proof";
docs: ["The merkle proof."];
type: {
vec: {
array: ["u8", 32];
};
};
}
];
};
},
{
name: "PayloadTypeLocal";
type: {
kind: "enum";
variants: [
{
name: "Pubkey";
fields: ["publicKey"];
},
{
name: "Seeds";
fields: [
{
defined: "SeedsVecLocal";
}
];
},
{
name: "MerkleProof";
fields: [
{
defined: "ProofInfoLocal";
}
];
},
{
name: "Number";
fields: ["u64"];
}
];
};
}
];
errors: [
{
code: 6000;
name: "PublicKeyMismatch";
msg: "PublicKeyMismatch";
},
{
code: 6001;
name: "InvalidMintAuthority";
msg: "InvalidMintAuthority";
},
{
code: 6002;
name: "UninitializedAccount";
msg: "UninitializedAccount";
},
{
code: 6003;
name: "IncorrectOwner";
msg: "IncorrectOwner";
},
{
code: 6004;
name: "PublicKeysShouldBeUnique";
msg: "PublicKeysShouldBeUnique";
},
{
code: 6005;
name: "StatementFalse";
msg: "StatementFalse";
},
{
code: 6006;
name: "NotRentExempt";
msg: "NotRentExempt";
},
{
code: 6007;
name: "NumericalOverflow";
msg: "NumericalOverflow";
},
{
code: 6008;
name: "ExpectedSolAccount";
msg: "Expected a sol account but got an spl token account instead";
},
{
code: 6009;
name: "CannotExchangeSOLForSol";
msg: "Cannot exchange sol for sol";
},
{
code: 6010;
name: "SOLWalletMustSign";
msg: "If paying with sol, sol wallet must be signer";
},
{
code: 6011;
name: "CannotTakeThisActionWithoutHyperspaceSignOff";
msg: "Cannot take this action without hyperspace signing too";
},
{
code: 6012;
name: "NoPayerPresent";
msg: "No payer present on this txn";
},
{
code: 6013;
name: "DerivedKeyInvalid";
msg: "Derived key invalid";
},
{
code: 6014;
name: "MetadataDoesntExist";
msg: "Metadata doesn't exist";
},
{
code: 6015;
name: "InvalidTokenAmount";
msg: "Invalid token amount";
},
{
code: 6016;
name: "BothPartiesNeedToAgreeToSale";
msg: "Both parties need to agree to this sale";
},
{
code: 6017;
name: "CannotMatchFreeSalesWithoutHyperspaceOrSellerSignoff";
msg: "Cannot match free sales unless the hyperspace or seller signs off";
},
{
code: 6018;
name: "SaleRequiresSigner";
msg: "This sale requires a signer";
},
{
code: 6019;
name: "OldSellerNotInitialized";
msg: "Old seller not initialized";
},
{
code: 6020;
name: "SellerATACannotHaveDelegate";
msg: "Seller ata cannot have a delegate set";
},
{
code: 6021;
name: "BuyerATACannotHaveDelegate";
msg: "Buyer ata cannot have a delegate set";
},
{
code: 6022;
name: "NoValidSignerPresent";
msg: "No valid signer present";
},
{
code: 6023;
name: "InvalidBasisPoints";
msg: "BP must be less than or equal to 10000";
},
{
code: 6024;
name: "InvalidBrokerInformation";
msg: "Broker information must match";
},
{
code: 6025;
name: "InvalidTokenAccount";
msg: "Token Account holding selling token must be owned by seller";
},
{
code: 6026;
name: "InvalidPermissionlessCancel";
msg: "Cannot permissionless cancel this trade state";
},
{
code: 6027;
name: "InvalidCollectionTradeStateType";
msg: "Invalid Collection Trade State Type";
},
{
code: 6028;
name: "InvalidCollectionTradeStateIdentifier";
msg: "Invalid Collection Trade State Identifier";
},
{
code: 6029;
name: "BumpSeedNotInHashMap";
msg: "Bump seed not in hash map.";
},
{
code: 6030;
name: "MetaplexTransferFailed";
msg: "Failed to transfer NFT using metaplex";
},
{
code: 6031;
name: "MetaplexDelegateFailed";
msg: "Failed to set Sale delegate on NFT using metaplex";
},
{
code: 6032;
name: "MetaplexRevokeFailed";
msg: "Failed to revoke Sale delegate on NFT using metaplex";
},
{
code: 6033;
name: "IncorrectTokenStandard";
msg: "Token standard must be Programmable NFT or Standard NFT";
},
{
code: 6034;
name: "PNFTDelegateSetAlready";
msg: "PNFT Delegate set already";
},
{
code: 6035;
name: "RoyaltyBPMismatch";
msg: "Royalty basis points mismatch";
}
];
};
export const hyperspaceIdl: Hyperspace = {
version: "0.1.0",
name: "hyperspace",
instructions: [
{
name: "initProgramAsSigner",
accounts: [
{
name: "wallet",
isMut: true,
isSigner: true,
},
{
name: "programAsSigner",
isMut: true,
isSigner: false,
},
{
name: "systemProgram",
isMut: false,
isSigner: false,
},
],
args: [
{
name: "programAsSignerBump",
type: "u8",
},
],
},
{
name: "updateHyperspace",
accounts: [
{
name: "payer",
isMut: false,
isSigner: true,
},
{
name: "authority",
isMut: false,
isSigner: true,
},
{
name: "newAuthority",
isMut: false,
isSigner: false,
},
{
name: "feeWithdrawalDestination",
isMut: true,
isSigner: false,
},
{
name: "treasuryWithdrawalDestination",
isMut: true,
isSigner: false,
},
{
name: "treasuryWithdrawalDestinationOwner",
isMut: false,
isSigner: false,
},
{
name: "hyperspace",
isMut: true,
isSigner: false,
},
{
name: "tokenProgram",
isMut: false,
isSigner: false,
},
{
name: "systemProgram",
isMut: false,
isSigner: false,
},
{
name: "ataProgram",
isMut: false,
isSigner: false,
},
{
name: "rent",
isMut: false,
isSigner: false,
},
],
args: [
{
name: "sellerFeeBasisPoints",
type: {
option: "u16",
},
},
{
name: "requiresSignOff",
type: {
option: "bool",
},
},
{
name: "canChangeSalePrice",
type: {
option: "bool",
},
},
],
},
{
name: "withdraw",
accounts: [
{
name: "wallet",
isMut: false,
isSigner: false,
},
{
name: "receiptAccount",
isMut: true,
isSigner: false,
},
{
name: "escrowPaymentAccount",
isMut: true,
isSigner: false,
},
{
name: "authority",
isMut: false,
isSigner: false,
},
{
name: "hyperspace",
isMut: false,
isSigner: false,
},
{
name: "tokenProgram",
isMut: false,
isSigner: false,
},
{
name: "systemProgram",
isMut: false,
isSigner: false,
},
{
name: "rent",
isMut: false,
isSigner: false,
},
],
args: [
{
name: "escrowPaymentBump",
type: "u8",
},
{
name: "amount",
type: "u64",
},
],
},
{
name: "deposit",
accounts: [
{
name: "wallet",
isMut: false,
isSigner: true,
},
{
name: "paymentAccount",
isMut: true,
isSigner: false,
},
{
name: "transferAuthority",
isMut: false,
isSigner: false,
},
{
name: "escrowPaymentAccount",
isMut: true,
isSigner: false,
},
{
name: "authority",
isMut: false,
isSigner: false,
},
{
name: "hyperspace",
isMut: false,
isSigner: false,
},
{
name: "hyperspaceFeeAccount",
isMut: true,
isSigner: false,
},
{
name: "tokenProgram",
isMut: false,
isSigner: false,
},
{
name: "systemProgram",
isMut: false,
isSigner: false,
},
{
name: "rent",
isMut: false,
isSigner: false,
},
],
args: [
{
name: "escrowPaymentBump",
type: "u8",
},
{
name: "amount",
type: "u64",
},
],
},
{
name: "permissionlessCancel",
accounts: [
{
name: "wallet",
isMut: false,
isSigner: true,
},
{
name: "tokenMint",
isMut: false,
isSigner: false,
},
{
name: "seller",
isMut: false,
isSigner: false,
},
{
name: "tokenAccount",
isMut: false,
isSigner: false,
},
{
name: "tradeState",
isMut: true,
isSigner: false,
},
{
name: "programAsSigner",
isMut: false,
isSigner: false,
},
],
args: [
{
name: "programAsSignerBump",
type: "u8",
},
{
name: "tradeStateBump",
type: "u8",
},
{
name: "tokenSize",
type: "u64",
},
],
},
{
name: "collectionCancelBuy",
accounts: [
{
name: "wallet",
isMut: true,
isSigner: false,
},
{
name: "identifierPubkey",
isMut: false,
isSigner: false,
},
{
name: "authority",
isMut: true,
isSigner: false,
},
{
name: "hyperspace",
isMut: false,
isSigner: false,
},
{
name: "collectionBuyerTradeState",
isMut: true,
isSigner: false,
},
{
name: "programAsSigner",
isMut: false,
isSigner: false,
},
{
name: "tokenProgram",
isMut: false,
isSigner: false,
},
],
args: [
{
name: "buyerPrice",
type: "u64",
},
{
name: "programAsSignerBump",
type: "u8",
},
{
name: "tradeStateBump",
type: "u8",
},
{
name: "collectionTradeStateType",
type: "u8",
},
{
name: "identifierIndex",
type: "u8",
},
],
},
{
name: "cancel",
accounts: [
{
name: "wallet",
isMut: true,
isSigner: false,
docs: ["CHECK OWNER OF TRADE_STATE TODO"],
},
{
name: "tokenMint",
isMut: false,
isSigner: false,
},
{
name: "tokenAccount",
isMut: true,
isSigner: false,
},
{
name: "metadata",
isMut: true,
isSigner: false,
},
{
name: "authority",
isMut: false,
isSigner: false,
},
{
name: "hyperspace",
isMut: false,
isSigner: false,
},
{
name: "tradeState",
isMut: true,
isSigner: false,
},
{
name: "tokenProgram",
isMut: false,
isSigner: false,
},
{
name: "systemProgram",
isMut: false,
isSigner: false,
},
{
name: "metadataProgram",
isMut: false,
isSigner: false,
},
{
name: "programAsSigner",
isMut: false,
isSigner: false,
},
{
name: "instructions",
isMut: false,
isSigner: false,
},
{
name: "tokenRecord",
isMut: true,
isSigner: false,
docs: ["CHECK no check needed"],
},
{
name: "editionAccount",
isMut: false,
isSigner: false,
docs: ["CHECK no check needed"],
},
{
name: "authorizationRules",
isMut: false,
isSigner: false,
docs: ["CHECK no check needed"],
},
{
name: "mplTokenAuthRulesProgram",
isMut: false,
isSigner: false,
docs: ["CHECK no check needed"],
},
],
args: [
{
name: "isBuy",
type: "u8",
},
{
name: "programAsSignerBump",
type: "u8",
},
{
name: "tradeStateBump",
type: "u8",
},
{
name: "tokenSize",
type: "u64",
},
],
},
{
name: "collectionExecuteSale",
accounts: [
{
name: "buyer",
isMut: true,
isSigner: false,
},
{
name: "buyerBrokerWallet",
isMut: true,
isSigner: false,
},
{
name: "seller",
isMut: true,
isSigner: false,
},
{
name: "sellerBrokerWallet",
isMut: true,
isSigner: false,
},
{
name: "tokenAccount",
isMut: true,
isSigner: false,
},
{
name: "tokenMint",
isMut: false,
isSigner: false,
},
{
name: "metadata",
isMut: true,
isSigner: false,
},
{
name: "editionData",
isMut: false,
isSigner: false,
},
{
name: "escrowPaymentAccount",
isMut: true,
isSigner: false,
},
{
name: "buyerReceiptTokenAccount",
isMut: true,
isSigner: false,
},
{
name: "authority",
isMut: false,
isSigner: false,
},
{
name: "hyperspace",
isMut: false,
isSigner: false,
},
{
name: "hyperspaceTreasury",
isMut: true,
isSigner: false,
},
{
name: "identifierPubkey",
isMut: false,
isSigner: false,
},
{
name: "collectionBuyerTradeState",
isMut: true,
isSigner: false,
},
{
name: "sellerTradeState",
isMut: true,
isSigner: false,
},
{
name: "tokenProgram",
isMut: false,
isSigner: false,
},
{
name: "systemProgram",
isMut: false,
isSigner: false,
},
{
name: "ataProgram",
isMut: false,
isSigner: false,
},
{
name: "metadataProgram",
isMut: false,
isSigner: false,
},
{
name: "mplTokenAuthRulesProgram",
isMut: false,
isSigner: false,
},
{
name: "programAsSigner",
isMut: false,
isSigner: false,
},
{
name: "rent",
isMut: false,
isSigner: false,
},
{
name: "instructions",
isMut: false,
isSigner: false,
},
{
name: "ownerTokenRecord",
isMut: true,
isSigner: false,
docs: ["CHECK no check needed"],
},
{
name: "destinationTokenRecord",
isMut: true,
isSigner: false,
docs: ["CHECK no check needed"],
},
{
name: "authorizationRules",
isMut: false,
isSigner: false,
docs: ["CHECK no check needed"],
},
],
args: [
{
name: "escrowPaymentBump",
type: "u8",
},
{
name: "programAsSignerBump",
type: "u8",
},
{
name: "buyerTradeStateBump",
type: "u8",
},
{
name: "sellerTradeStateBump",
type: "u8",
},
{
name: "maxAmountToPay",
type: "u64",
},
{
name: "buyerBrokerBasisPoints",
type: "u16",
},
{
name: "sellerBrokerBasisPoints",
type: "u16",
},
{
name: "tokenSize",
type: "u64",
},
{
name: "collectionTradeStateType",
type: "u8",
},
{
name: "identifierIndex",
type: "u8",
},
{
name: "royaltyBasisPoints",
type: "u16",
},
],
},
{
name: "executeSale",
accounts: [
{
name: "buyer",
isMut: true,
isSigner: false,
},
{
name: "buyerBrokerWallet",
isMut: true,
isSigner: false,
},
{
name: "seller",
isMut: true,
isSigner: false,
},
{
name: "sellerBrokerWallet",
isMut: true,
isSigner: false,
},
{
name: "tokenAccount",
isMut: true,
isSigner: false,
},
{
name: "tokenMint",
isMut: false,
isSigner: false,
},
{
name: "metadata",
isMut: true,
isSigner: false,
},
{
name: "escrowPaymentAccount",
isMut: true,
isSigner: false,
},
{
name: "buyerReceiptTokenAccount",
isMut: true,
isSigner: false,
},
{
name: "authority",
isMut: false,
isSigner: false,
},
{
name: "hyperspace",
isMut: false,
isSigner: false,
},
{
name: "hyperspaceTreasury",
isMut: true,
isSigner: false,
},
{
name: "buyerTradeState",
isMut: true,
isSigner: false,
},
{
name: "sellerTradeState",
isMut: true,
isSigner: false,
},
{
name: "tokenProgram",
isMut: false,
isSigner: false,
},
{
name: "systemProgram",
isMut: false,
isSigner: false,
},
{
name: "ataProgram",
isMut: false,
isSigner: false,
},
{
name: "metadataProgram",
isMut: false,
isSigner: false,
},
{
name: "mplTokenAuthRulesProgram",
isMut: false,
isSigner: false,
},
{
name: "programAsSigner",
isMut: false,
isSigner: false,
},
{
name: "rent",
isMut: false,
isSigner: false,
},
{
name: "instructions",
isMut: false,
isSigner: false,
},
{
name: "editionAccount",
isMut: false,
isSigner: false,
docs: ["CHECK no check needed"],
},
{
name: "ownerTokenRecord",
isMut: true,
isSigner: false,
docs: ["CHECK no check needed"],
},
{
name: "destinationTokenRecord",
isMut: true,
isSigner: false,
docs: ["CHECK no check needed"],
},
{
name: "authorizationRules",
isMut: false,
isSigner: false,
docs: ["CHECK no check needed"],
},
],
args: [
{
name: "escrowPaymentBump",
type: "u8",
},
{
name: "programAsSignerBump",
type: "u8",
},
{
name: "buyerTradeStateBump",
type: "u8",
},
{
name: "sellerTradeStateBump",
type: "u8",
},
{
name: "maxAmountToPay",
type: "u64",
},
{
name: "buyerBrokerBasisPoints",
type: "u16",
},
{
name: "sellerBrokerBasisPoints",
type: "u16",
},
{
name: "tokenSize",
type: "u64",
},
{
name: "royaltyBasisPoints",
type: "u16",
},
],
},
{
name: "sell",
accounts: [
{
name: "wallet",
isMut: false,
isSigner: true,
},
{
name: "sellerBrokerWallet",
isMut: false,
isSigner: false,
},
{
name: "tokenMint",
isMut: false,
isSigner: false,
},
{
name: "tokenAccount",
isMut: true,
isSigner: false,
},
{
name: "metadata",
isMut: true,
isSigner: false,
},
{
name: "authority",
isMut: false,
isSigner: false,
},
{
name: "hyperspace",
isMut: false,
isSigner: false,
},
{
name: "hyperspaceFeeAccount",
isMut: true,
isSigner: false,
},
{
name: "sellerTradeState",
isMut: true,
isSigner: false,
},
{
name: "tokenProgram",
isMut: false,
isSigner: false,
},
{
name: "systemProgram",
isMut: false,
isSigner: false,
},
{
name: "metadataProgram",
isMut: false,
isSigner: false,
},
{
name: "programAsSigner",
isMut: false,
isSigner: false,
},
{
name: "instructions",
isMut: false,
isSigner: false,
},
{
name: "tokenRecord",
isMut: true,
isSigner: false,
docs: ["CHECK no check needed"],
},
{
name: "editionAccount",
isMut: false,
isSigner: false,
docs: ["CHECK no check needed"],
},
{
name: "authorizationRules",
isMut: false,
isSigner: false,
docs: ["CHECK no check needed"],
},
{
name: "mplTokenAuthRulesProgram",
isMut: false,
isSigner: false,
docs: ["CHECK no check needed"],
},
{
name: "clock",
isMut: false,
isSigner: false,
},
],
args: [
{
name: "tradeStateBump",
type: "u8",
},
{
name: "programAsSignerBump",
type: "u8",
},
{
name: "minAmountToReceive",
type: "u64",
},
{
name: "brokerBasisPoints",
type: "u16",
},
{
name: "tokenSize",
type: "u64",
},
{
name: "royaltyBasisPoints",
type: "u16",
},
],
},
{
name: "buy",
accounts: [
{
name: "wallet",
isMut: false,
isSigner: true,
},
{
name: "buyerBrokerWallet",
isMut: false,
isSigner: false,
},
{
name: "paymentAccount",
isMut: true,
isSigner: false,
},
{
name: "transferAuthority",
isMut: false,
isSigner: false,
},
{
name: "tokenAccount",
isMut: false,
isSigner: false,
},
{
name: "buyerReceiptTokenAccount",
isMut: false,
isSigner: false,
},
{
name: "metadata",
isMut: false,
isSigner: false,
},
{
name: "escrowPaymentAccount",
isMut: true,
isSigner: false,
},
{
name: "authority",
isMut: false,
isSigner: false,
},
{
name: "hyperspace",
isMut: false,
isSigner: false,
},
{
name: "hyperspaceFeeAccount",
isMut: true,
isSigner: false,
},
{
name: "buyerTradeState",
isMut: true,
isSigner: false,
},
{
name: "tokenProgram",
isMut: false,
isSigner: false,
},
{
name: "systemProgram",
isMut: false,
isSigner: false,
},
{
name: "rent",
isMut: false,
isSigner: false,
},
{
name: "clock",
isMut: false,
isSigner: false,
},
],
args: [
{
name: "tradeStateBump",
type: "u8",
},
{
name: "escrowPaymentBump",
type: "u8",
},
{
name: "maxAmountToPay",
type: "u64",
},
{
name: "brokerBasisPoints",
type: "u16",
},
{
name: "tokenSize",
type: "u64",
},
{
name: "royaltyBasisPoints",
type: "u16",
},
],
},
{
name: "collectionBuy",
accounts: [
{
name: "wallet",
isMut: true,
isSigner: true,
},
{
name: "buyerBrokerWallet",
isMut: false,
isSigner: false,
},
{
name: "identifierPubkey",
isMut: false,
isSigner: false,
},
{
name: "escrowPaymentAccount",
isMut: true,
isSigner: false,
},
{
name: "authority",
isMut: false,
isSigner: false,
},
{
name: "hyperspace",
isMut: false,
isSigner: false,
},
{
name: "hyperspaceFeeAccount",
isMut: true,
isSigner: false,
},
{
name: "collectionBuyerTradeState",
isMut: true,
isSigner: false,
},
{
name: "tokenProgram",
isMut: false,
isSigner: false,
},
{
name: "systemProgram",
isMut: false,
isSigner: false,
},
{
name: "rent",
isMut: false,
isSigner: false,
},
{
name: "clock",
isMut: false,
isSigner: false,
},
],
args: [
{
name: "tradeStateBump",
type: "u8",
},
{
name: "escrowPaymentBump",
type: "u8",
},
{
name: "buyerPrice",
type: "u64",
},
{
name: "brokerBasisPoints",
type: "u16",
},
{
name: "collectionTradeStateType",
type: "u8",
},
{
name: "identifierIndex",
type: "u8",
},
{
name: "royaltyBasisPoints",
type: "u16",
},
],
},
{
name: "createCollectionBuyTradeState",
accounts: [
{
name: "wallet",
isMut: true,
isSigner: true,
},
{
name: "identifierPubkey",
isMut: false,
isSigner: false,
},
{
name: "brokerWallet",
isMut: false,
isSigner: false,
},
{
name: "tradeState",
isMut: true,
isSigner: false,
},
{
name: "systemProgram",
isMut: false,
isSigner: false,
},
{
name: "clock",
isMut: false,
isSigner: false,
},
],
args: [
{
name: "tradeStateBump",
type: "u8",
},
{
name: "buyPrice",
type: "u64",
},
{
name: "brokerBasisPoints",
type: "u16",
},
{
name: "collectionTradeStateType",
type: "u8",
},
{
name: "identifierIndex",
type: "u8",
},
{
name: "royaltyBasisPoints",
type: "u16",
},
],
},
{
name: "createTradeState",
accounts: [
{
name: "wallet",
isMut: true,
isSigner: true,
},
{
name: "collection",
isMut: false,
isSigner: false,
},
{
name: "brokerWallet",
isMut: false,
isSigner: false,
},
{
name: "tokenAccount",
isMut: false,
isSigner: false,
},
{
name: "tokenMint",
isMut: false,
isSigner: false,
},
{
name: "tradeState",
isMut: true,
isSigner: false,
},
{
name: "systemProgram",
isMut: false,
isSigner: false,
},
{
name: "clock",
isMut: false,
isSigner: false,
},
],
args: [
{
name: "isBuy",
type: "u8",
},
{
name: "tradeStateBump",
type: "u8",
},
{
name: "buyPrice",
type: "u64",
},
{
name: "brokerBasisPoints",
type: "u16",
},
{
name: "tokenSize",
type: "u64",
},
{
name: "royaltyBasisPoints",
type: "u16",
},
],
},
],
accounts: [
{
name: "hyperspace",
type: {
kind: "struct",
fields: [
{
name: "hyperspaceFeeAccount",
type: "publicKey",
},
{
name: "hyperspaceTreasury",
type: "publicKey",
},
{
name: "treasuryWithdrawalDestination",
type: "publicKey",
},
{
name: "feeWithdrawalDestination",
type: "publicKey",
},
{
name: "authority",
type: "publicKey",
},
{
name: "creator",
type: "publicKey",
},
{
name: "bump",
type: "u8",
},
{
name: "treasuryBump",
type: "u8",
},
{
name: "feePayerBump",
type: "u8",
},
{
name: "sellerFeeBasisPoints",
type: "u16",
},
{
name: "requiresSignOff",
type: "bool",
},
{
name: "canChangeSalePrice",
type: "bool",
},
],
},
},
{
name: "programAsSigner",
type: {
kind: "struct",
fields: [
{
name: "firstByte",
type: "u8",
},
],
},
},
{
name: "tradeState",
type: {
kind: "struct",
fields: [
{
name: "buyPrice",
type: "u64",
},
{
name: "userWallet",
type: "publicKey",
},
{
name: "isBuy",
type: "u8",
},
{
name: "collection",
type: "publicKey",
},
{
name: "brokerWallet",
type: "publicKey",
},
{
name: "brokerBasisPoints",
type: "u16",
},
{
name: "tokenMint",
type: "publicKey",
},
{
name: "royaltyBasisPoints",
type: "u16",
},
{
name: "timestamp",
type: "i64",
},
],
},
},
{
name: "collectionBuyTradeState",
type: {
kind: "struct",
fields: [
{
name: "userWallet",
type: "publicKey",
},
{
name: "collectionTradeStateType",
type: "u8",
},
{
name: "identifierIndex",
type: "u8",
},
{
name: "identifierPubkey",
type: "publicKey",
},
{
name: "brokerWallet",
type: "publicKey",
},
{
name: "brokerBasisPoints",
type: "u16",
},
{
name: "royaltyBasisPoints",
type: "u16",
},
{
name: "timestamp",
type: "i64",
},
],
},
},
],
types: [
{
name: "AuthorizationDataLocal",
type: {
kind: "struct",
fields: [
{
name: "payload",
type: {
vec: {
defined: "TaggedPayload",
},
},
},
],
},
},
{
name: "TaggedPayload",
type: {
kind: "struct",
fields: [
{
name: "name",
type: "string",
},
{
name: "payload",
type: {
defined: "PayloadTypeLocal",
},
},
],
},
},
{
name: "SeedsVecLocal",
type: {
kind: "struct",
fields: [
{
name: "seeds",
docs: ["The vector of derivation seeds."],
type: {
vec: "bytes",
},
},
],
},
},
{
name: "ProofInfoLocal",
type: {
kind: "struct",
fields: [
{
name: "proof",
docs: ["The merkle proof."],
type: {
vec: {
array: ["u8", 32],
},
},
},
],
},
},
{
name: "PayloadTypeLocal",
type: {
kind: "enum",
variants: [
{
name: "Pubkey",
fields: ["publicKey"],
},
{
name: "Seeds",
fields: [
{
defined: "SeedsVecLocal",
},
],
},
{
name: "MerkleProof",
fields: [
{
defined: "ProofInfoLocal",
},
],
},
{
name: "Number",
fields: ["u64"],
},
],
},
},
],
errors: [
{
code: 6000,
name: "PublicKeyMismatch",
msg: "PublicKeyMismatch",
},
{
code: 6001,
name: "InvalidMintAuthority",
msg: "InvalidMintAuthority",
},
{
code: 6002,
name: "UninitializedAccount",
msg: "UninitializedAccount",
},
{
code: 6003,
name: "IncorrectOwner",
msg: "IncorrectOwner",
},
{
code: 6004,
name: "PublicKeysShouldBeUnique",
msg: "PublicKeysShouldBeUnique",
},
{
code: 6005,
name: "StatementFalse",
msg: "StatementFalse",
},
{
code: 6006,
name: "NotRentExempt",
msg: "NotRentExempt",
},
{
code: 6007,
name: "NumericalOverflow",
msg: "NumericalOverflow",
},
{
code: 6008,
name: "ExpectedSolAccount",
msg: "Expected a sol account but got an spl token account instead",
},
{
code: 6009,
name: "CannotExchangeSOLForSol",
msg: "Cannot exchange sol for sol",
},
{
code: 6010,
name: "SOLWalletMustSign",
msg: "If paying with sol, sol wallet must be signer",
},
{
code: 6011,
name: "CannotTakeThisActionWithoutHyperspaceSignOff",
msg: "Cannot take this action without hyperspace signing too",
},
{
code: 6012,
name: "NoPayerPresent",
msg: "No payer present on this txn",
},
{
code: 6013,
name: "DerivedKeyInvalid",
msg: "Derived key invalid",
},
{
code: 6014,
name: "MetadataDoesntExist",
msg: "Metadata doesn't exist",
},
{
code: 6015,
name: "InvalidTokenAmount",
msg: "Invalid token amount",
},
{
code: 6016,
name: "BothPartiesNeedToAgreeToSale",
msg: "Both parties need to agree to this sale",
},
{
code: 6017,
name: "CannotMatchFreeSalesWithoutHyperspaceOrSellerSignoff",
msg: "Cannot match free sales unless the hyperspace or seller signs off",
},
{
code: 6018,
name: "SaleRequiresSigner",
msg: "This sale requires a signer",
},
{
code: 6019,
name: "OldSellerNotInitialized",
msg: "Old seller not initialized",
},
{
code: 6020,
name: "SellerATACannotHaveDelegate",
msg: "Seller ata cannot have a delegate set",
},
{
code: 6021,
name: "BuyerATACannotHaveDelegate",
msg: "Buyer ata cannot have a delegate set",
},
{
code: 6022,
name: "NoValidSignerPresent",
msg: "No valid signer present",
},
{
code: 6023,
name: "InvalidBasisPoints",
msg: "BP must be less than or equal to 10000",
},
{
code: 6024,
name: "InvalidBrokerInformation",
msg: "Broker information must match",
},
{
code: 6025,
name: "InvalidTokenAccount",
msg: "Token Account holding selling token must be owned by seller",
},
{
code: 6026,
name: "InvalidPermissionlessCancel",
msg: "Cannot permissionless cancel this trade state",
},
{
code: 6027,
name: "InvalidCollectionTradeStateType",
msg: "Invalid Collection Trade State Type",
},
{
code: 6028,
name: "InvalidCollectionTradeStateIdentifier",
msg: "Invalid Collection Trade State Identifier",
},
{
code: 6029,
name: "BumpSeedNotInHashMap",
msg: "Bump seed not in hash map.",
},
{
code: 6030,
name: "MetaplexTransferFailed",
msg: "Failed to transfer NFT using metaplex",
},
{
code: 6031,
name: "MetaplexDelegateFailed",
msg: "Failed to set Sale delegate on NFT using metaplex",
},
{
code: 6032,
name: "MetaplexRevokeFailed",
msg: "Failed to revoke Sale delegate on NFT using metaplex",
},
{
code: 6033,
name: "IncorrectTokenStandard",
msg: "Token standard must be Programmable NFT or Standard NFT",
},
{
code: 6034,
name: "PNFTDelegateSetAlready",
msg: "PNFT Delegate set already",
},
{
code: 6035,
name: "RoyaltyBPMismatch",
msg: "Royalty basis points mismatch",
},
],
};
================================================
FILE: chatgpt-plugin/src/lib/middleware.ts
================================================
import { NextApiRequest, NextApiResponse, NextApiHandler } from "next";
import { resolveAddress, resolveToken } from "./address";
export type Options = {
addresses?: string[];
tokens?: string[];
rewriteQuery?: boolean;
};
export function makeApiPostRequest(handler: NextApiHandler, options?: Options): NextApiHandler {
return async (req: NextApiRequest, res: NextApiResponse) => {
const rewriteQuery = options ? options.rewriteQuery : false;
if (req.method != "POST") {
res.status(405).send({ message: "Only POST requests allowed" });
return;
}
const target = rewriteQuery ? req.query : req.body;
try {
if (options && options.addresses) {
for (const addr of options.addresses) {
const publicKey = await resolveAddress(target[addr]);
target[addr] = publicKey;
}
}
if (options && options.tokens) {
for (const token of options.tokens) {
const publicKey = await resolveToken(target[token]);
target[token] = publicKey;
}
}
} catch (e) {
res.status(400).send({ message: (e as Error).toString() });
return;
}
await handler(req, res);
};
}
================================================
FILE: chatgpt-plugin/src/lib/on-chain-metadata/index.ts
================================================
import * as anchor from "@coral-xyz/anchor";
import { Connection } from "@solana/web3.js";
type SolanaPayTx = {
transaction: string;
message?: string;
};
/**
* Need this to fake a wallet with only a publicKey for the anchor provider
*/
class FakeWallet {
publicKey: anchor.web3.PublicKey;
constructor(publicKey: anchor.web3.PublicKey) {
this.publicKey = publicKey;
}
async signTransaction<
T extends anchor.web3.Transaction | anchor.web3.VersionedTransaction
>(tx: T): Promise {
return tx;
}
async signAllTransactions<
T extends anchor.web3.Transaction | anchor.web3.VersionedTransaction
>(txs: T[]): Promise {
return txs;
}
}
export async function createWriteNFTMetadataTx(
connection: Connection,
owner: string,
metadata: Object
): Promise {
const program = await anchor.Program.at(
process.env.ON_CHAIN_METADATA_PROGRAM as string,
new anchor.AnchorProvider(
connection,
new FakeWallet(new anchor.web3.PublicKey(owner)),
{}
)
);
let metadataBytes = JSON.stringify(metadata);
console.log(metadataBytes.length);
if (metadataBytes.length > 500) {
throw new Error("Metadata too large: " + metadataBytes.length);
}
const metadataKp = anchor.web3.Keypair.generate();
// Create metadata
let initIx = await program.methods
.initialize(new anchor.BN(metadataBytes.length))
.accounts({
owner,
metadata: metadataKp.publicKey,
})
.signers([metadataKp])
.instruction();
// Write metadata
let writeIx = await program.methods
.write(new anchor.BN(0), Buffer.from(metadataBytes))
.accounts({
metadata: metadataKp.publicKey,
owner,
})
.instruction();
// Validate metadata
let validateIx = await program.methods
.validate()
.accounts({ metadata: metadataKp.publicKey })
.instruction();
let tx = new anchor.web3.Transaction();
tx = tx.add(initIx).add(writeIx).add(validateIx);
tx.recentBlockhash = (await connection.getRecentBlockhash()).blockhash;
tx.feePayer = new anchor.web3.PublicKey(owner);
tx.partialSign(metadataKp);
return {
transaction: tx
.serialize({ requireAllSignatures: false })
.toString("base64"),
};
}
export async function createCloseNFTMetadataTx(
connection: Connection,
owner: string,
account: string
): Promise {
const program = await anchor.Program.at(
process.env.ON_CHAIN_METADATA_PROGRAM as string,
new anchor.AnchorProvider(
connection,
new FakeWallet(new anchor.web3.PublicKey(owner)),
{}
)
);
let tx = await program.methods
.close()
.accounts({
metadata: account,
recipient: owner,
owner,
})
.transaction();
tx.recentBlockhash = (await connection.getRecentBlockhash()).blockhash;
tx.feePayer = new anchor.web3.PublicKey(owner);
return {
transaction: tx
.serialize({ requireAllSignatures: false })
.toString("base64"),
};
}
================================================
FILE: chatgpt-plugin/src/pages/api/constants.ts
================================================
import express from "express";
import { RestClient } from "@hellomoon/api";
import { Connection } from "@solana/web3.js";
import { HyperspaceClient } from "hyperspace-client-js";
export const APP = express();
export const PORT = process.env.PORT || 3333;
import dotenv from "dotenv";
dotenv.config();
// Internal Solana Pay constants
export const SOLANA_PAY_LABEL = "Solana Labs ChatGPT Plugin";
export const TRANSACTION_ENDPOINTS = [
"createBuyNFT",
"createWriteNFTMetadata",
"createCloseNFTMetadata",
];
export type TransactionEndpoints = (typeof TRANSACTION_ENDPOINTS)[number];
export const TX_DESCRIPTIONS: Record = {
createBuyNFT: "Sign to Buy NFT",
createWriteNFTMetadata: "Sign to Write NFT Metadata",
createCloseNFTMetadata: "Sign to Close NFT Metadata",
createTransferToken: "Sign to Transfer Token",
createTransferSol: "Sign to Transfer Sol",
};
// Inferred Constants
export let HELIUS_URL: string;
export let SELF_URL: string;
export let HYPERSPACE_CLIENT: HyperspaceClient;
export let HELLOMOON_CLIENT: RestClient;
export let CONNECTION: Connection;
export default function index() {
HELIUS_URL = `https://rpc.helius.xyz/?api-key=${process.env.HELIUS_API_KEY}`;
CONNECTION = new Connection(HELIUS_URL);
SELF_URL = process.env.SELF_URL as string;
HYPERSPACE_CLIENT = new HyperspaceClient(process.env.HYPERSPACE_API_KEY as string);
HELLOMOON_CLIENT = new RestClient(process.env.HELLOMOON_API_KEY as string);
}
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/getAccountInfo/index.ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import { PublicKey, Connection, Keypair } from "@solana/web3.js";
import { Program, AnchorProvider, BorshAccountsCoder, BN } from "@coral-xyz/anchor";
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import configConstants, { CONNECTION } from "../../constants";
import { makeApiPostRequest } from "@/lib/middleware";
configConstants();
/**
* Replace Anchor data (BNs, PublicKeys) with stringified data
* @param obj
* @returns
*/
export function stringifyAnchorObject(obj: any): any {
if (obj === true || obj === false) {
return obj;
} else if (!obj) {
return {};
} else if (obj instanceof BN) {
return obj.toString();
} else if (obj instanceof PublicKey) {
return obj.toString();
} else if (typeof obj === "bigint") {
let bigInt = obj as BigInt;
return bigInt.toString();
}
if (typeof obj === "object" && obj !== null) {
return Object.keys(obj).reduce((acc: Record, key: string) => {
acc[key] = stringifyAnchorObject(obj[key]);
return acc;
}, {});
}
return obj;
}
/**
* Returns accountInfo or extends it with deserialized account data if the account is a program account of an Anchor program
* @param accountAddress
* @returns
*/
async function getParsedAccountInfo(connection: Connection, accountAddress: PublicKey) {
// TODO: copy the explorer code here that manually deserializes a bunch of stuff, like Mango & Pyth
const accountInfo = (await connection.getAccountInfo(accountAddress)) as any;
// If acccount is not a program, check for Anchor IDL
if (accountInfo?.owner && !accountInfo.executable) {
try {
const program = await Program.at(
accountInfo.owner,
new AnchorProvider(connection, new NodeWallet(Keypair.generate()), {
commitment: "confirmed",
}),
);
// Search through Anchor IDL for the account type
const rawData = accountInfo.data;
const coder = new BorshAccountsCoder(program.idl);
const accountDefTmp = program.idl.accounts?.find((accountType: any) =>
(rawData as Buffer)
.slice(0, 8)
.equals(BorshAccountsCoder.accountDiscriminator(accountType.name)),
);
// If we found the Anchor IDL type, decode the account state
if (accountDefTmp) {
const accountDef = accountDefTmp;
// Decode the anchor data & stringify the data
const decodedAccountData = stringifyAnchorObject(coder.decode(accountDef.name, rawData));
let payload = {
...accountInfo,
extended: decodedAccountData,
};
delete payload["data"];
return payload;
}
} catch (err) {
console.log(err);
}
}
return accountInfo || {};
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
const accountInfo = await getParsedAccountInfo(CONNECTION, req.body["address"]);
res.status(200).json(accountInfo);
}
export default makeApiPostRequest(handler, { addresses: ["address"] });
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/getAssetsByOwner/index.ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import configConstants, { HELIUS_URL } from "../../constants";
configConstants();
import axios from "axios";
import { makeApiPostRequest } from "@/lib/middleware";
/**
* Returns the data from the Metaplex Read API
* @param address
* @param page (optional) page number
* @param limit (optional) set to 5 to prevent overflowing GPT context window
* @returns
*/
export const readApiGetAssetsByOwner = async (
address: string,
page: number = 1,
limit: number = 5,
) => {
const sortBy = {
sortBy: "created",
sortDirection: "asc",
};
const before = "";
const after = "";
const { data } = await axios.post(HELIUS_URL, {
jsonrpc: "2.0",
id: "my-id",
method: "getAssetsByOwner",
params: [address, sortBy, limit, page, before, after],
});
return data.result;
};
async function handler(req: NextApiRequest, res: NextApiResponse) {
const assets = await readApiGetAssetsByOwner(req.body["address"].toString());
res.status(200).send(assets);
}
export default makeApiPostRequest(handler, { addresses: ["address"] });
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/getBalance/index.ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
import configConstants, { CONNECTION } from "../../constants";
configConstants();
import { makeApiPostRequest } from "@/lib/middleware";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { address } = req.body;
const balance = await CONNECTION.getBalance(new PublicKey(address));
res.status(200).send({ sol: balance / LAMPORTS_PER_SOL });
}
export default makeApiPostRequest(handler, { addresses: ["address"] });
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/getCollectionsByFloorPrice/index.ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes";
import configConstants, { HYPERSPACE_CLIENT } from "../../constants";
import { makeApiPostRequest } from "@/lib/middleware";
configConstants();
type CollectionStats = {
id: string;
desc: string;
img: string;
website: string;
floor_price: number;
};
/**
* Provides a feed of NFT collections that the user can afford.
*/
async function hyperspaceGetCollectionsByFloorPrice(
maxFloorPrice: number | undefined,
minFloorPrice: number | undefined,
pageSize: number = 5,
orderBy: string = "DESC",
humanReadableSlugs: boolean = false,
) {
let pageNumber = 1;
let results: CollectionStats[] = [];
let hasMore = true;
const PAGE_SCRAPE_LIMIT = 10;
while (results.length < pageSize && pageNumber < PAGE_SCRAPE_LIMIT && hasMore) {
let projects = await HYPERSPACE_CLIENT.getProjects({
condition: {
floorPriceFilter: {
min: minFloorPrice ?? null,
max: maxFloorPrice ?? null,
},
},
orderBy: {
field_name: "floor_price",
sort_order: orderBy.toLocaleUpperCase() as any,
},
paginationInfo: {
page_size: 512,
page_number: pageNumber,
},
});
let stats: CollectionStats[] =
projects.getProjectStats.project_stats
?.filter(project => {
return (project.volume_7day ?? 0 > 0) && (project.floor_price ?? 0 > 0);
})
.map(project => {
return {
id: project.project_id,
desc: project.project?.display_name ?? "",
img: project.project?.img_url ?? "",
website: project.project?.website ?? "",
floor_price: project.floor_price ?? 0,
};
}) ?? [];
if (humanReadableSlugs) {
stats = stats?.filter(stat => {
try {
bs58.decode(stat.id!);
return false;
} catch (err) {
return true;
}
});
}
pageNumber += 1;
console.log("\tFetching collection info... ", stats.length, pageNumber);
results = results.concat(stats!);
hasMore = projects.getProjectStats.pagination_info.has_next_page;
}
return {
projects: results.slice(0, pageSize),
hasMore: hasMore,
};
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { maxFloorPrice, minFloorPrice, orderBy, pageSize, humanReadable } = req.body;
const result = await hyperspaceGetCollectionsByFloorPrice(
maxFloorPrice,
minFloorPrice,
pageSize,
orderBy,
humanReadable,
);
res.status(200).send(JSON.stringify(result));
}
export default makeApiPostRequest(handler);
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/getCollectionsByName/index.ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import configConstants, { HYPERSPACE_CLIENT } from "../../constants";
configConstants();
import { makeApiPostRequest } from "@/lib/middleware";
async function getCollectionsByName(req: NextApiRequest, res: NextApiResponse) {
const { projectName } = req.body;
const projects = await HYPERSPACE_CLIENT.searchProjectByName({
condition: {
// In practice, it was harder to find projects based on this condition
// matchName: {
// operation: "FUZZY" as StringInputOperationEnum.Fuzzy,
// value: projectName as string,
// },
name: projectName as string,
},
});
const paredResponse =
projects.getProjectStatByName.project_stats
?.map(project => {
return {
id: project.project_id,
verified: project.project?.is_verified ?? false,
name: project.project?.display_name ?? "",
img: project.project?.img_url ?? "",
website: project.project?.website ?? "",
floorPrice: project.floor_price ?? 0,
twitterFollowers: project.twitter_followers ?? 0,
twitter: project.project?.twitter ?? "",
};
})
.filter(project => project.verified) ?? [];
const result = {
projects: paredResponse,
};
res.status(200).send(JSON.stringify(result));
}
export default makeApiPostRequest(getCollectionsByName);
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/getListedCollectionNFTs/index.ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import configConstants, { HYPERSPACE_CLIENT } from "../../constants";
import { makeApiPostRequest } from "@/lib/middleware";
configConstants();
type NFTListing = {
price: number;
image: string;
token: string;
};
type ListedNFTResponse = {
listings: NFTListing[];
hasMore: boolean;
};
async function hyperspaceGetListedCollectionNFTs(
projectId: string,
pageSize: number = 5,
priceOrder: string = "DESC",
): Promise {
let listedNFTs: NFTListing[] = [];
let hasMore = true;
let pageNumber = 1;
let pagesScraped = 0;
const PAGE_SCRAPE_LIMIT = 10;
while (listedNFTs.length < pageSize && hasMore && pagesScraped < PAGE_SCRAPE_LIMIT) {
let results = await HYPERSPACE_CLIENT.getMarketplaceSnapshot({
condition: {
projects: [{ project_id: projectId }],
onlyListings: true,
},
orderBy: {
field_name: "lowest_listing_price",
sort_order: priceOrder.toLocaleUpperCase() as any,
},
paginationInfo: {
page_number: pageNumber,
},
});
let snaps = results.getMarketPlaceSnapshots.market_place_snapshots!;
let orderedListings = snaps.sort(
(a, b) => a.lowest_listing_mpa!.price! - b.lowest_listing_mpa!.price!,
);
pageNumber += 1;
let crucialInfo: NFTListing[] = orderedListings
.filter(
arr =>
// We filter out Magic Eden's marketplace because they
// require an API key to make purchases programmatically
arr.lowest_listing_mpa?.marketplace_program_id !==
"M2mx93ekt1fmXSVkTrUL9xVFHkmME8HTUi5Cyc5aF7K",
)
.map(arr => {
return {
price: arr.lowest_listing_mpa!.price!,
token: arr.token_address,
image: arr.meta_data_img ?? "",
marketplace: arr.lowest_listing_mpa!.marketplace_program_id!,
};
});
listedNFTs = listedNFTs.concat(crucialInfo);
hasMore = results.getMarketPlaceSnapshots.pagination_info.has_next_page;
}
return {
listings: listedNFTs.slice(0, pageSize),
hasMore,
};
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { projectId, pageSize, priceOrder } = req.body;
const result = await hyperspaceGetListedCollectionNFTs(projectId, pageSize, priceOrder);
res.status(200).send(JSON.stringify(result));
}
export default makeApiPostRequest(handler);
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/getSignaturesForAddress/index.ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import { PublicKey } from "@solana/web3.js";
import configConstants, { CONNECTION } from "../../constants";
import { makeApiPostRequest } from "@/lib/middleware";
configConstants();
async function handler(req: NextApiRequest, res: NextApiResponse) {
const accountAddress = req.body["address"];
const beforeSig = req.body.beforeSignature ?? "";
const untilSig = req.body.untilSignature ?? "";
const signatures = await CONNECTION.getSignaturesForAddress(accountAddress, {
limit: 11,
before: beforeSig.length > 0 ? beforeSig : undefined,
until: untilSig.length > 0 ? untilSig : undefined,
});
res.status(200).send({
hasMore: signatures.length === 11,
nextPage: signatures.length === 11 ? { beforeSignature: signatures[10].signature } : null,
signatures: signatures.map(sig => {
return {
slot: sig.slot,
signature: sig.signature,
err: sig.err ?? undefined,
memo: sig.memo ?? undefined,
confirmationStatus: sig.confirmationStatus,
blockTime: new Date((sig.blockTime as number) * 1000).toDateString(),
};
}),
});
}
export default makeApiPostRequest(handler, { addresses: ["address"] });
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/getTokenAccounts/index.ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import configConstants, { CONNECTION } from "../../constants";
import { makeApiPostRequest } from "@/lib/middleware";
configConstants();
type TokenInfo = {
mint: string;
amount: string;
};
async function handler(req: NextApiRequest, res: NextApiResponse) {
let result = await CONNECTION.getParsedTokenAccountsByOwner(
req.body["address"],
{ programId: TOKEN_PROGRAM_ID },
"confirmed",
);
const tokenInfos: TokenInfo[] = [];
for (const accountInfo of result.value) {
const info = accountInfo.account.data.parsed.info;
if (info.tokenAmount.uiAmount !== 0) {
tokenInfos.push({
mint: info.mint.toString(),
amount: info.tokenAmount.uiAmountString,
});
}
}
res.status(200).json(tokenInfos);
}
export default makeApiPostRequest(handler, { addresses: ["address"] });
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/getTotalValue/index.ts
================================================
import { TokenPriceBatchedRequest, NftMintPriceByCreatorAvgRequest } from "@hellomoon/api";
import { PublicKey } from "@solana/web3.js";
import { NextApiRequest, NextApiResponse } from "next";
import { readApiGetAssetsByOwner } from "../getAssetsByOwner";
import configConstants, { CONNECTION, HELLOMOON_CLIENT } from "../../constants";
import { makeApiPostRequest } from "@/lib/middleware";
configConstants();
async function getTokenTotal(address: string) {
let connection = CONNECTION;
let tokenInfos = await (
await fetch(
`https://api.helius.xyz/v0/addresses/${address}/balances?api-key=${process.env.HELIUS_API_KEY}`,
)
).json();
let mints: string[] = [];
let mintAmount: Record = {};
for (const info of tokenInfos["tokens"] as any) {
if (info.amount !== "0") {
mints.push(info.mint);
mintAmount[info.mint] = Number.parseInt(info.amount);
}
}
let mintPrice: Record = {};
let mintVolume: Record = {};
let pricePromises: Promise[] = [];
let decimalPromises: Promise[] = [];
let counter = 0;
let batchSize = 10;
while (counter < mints.length) {
pricePromises.push(
HELLOMOON_CLIENT.send(
new TokenPriceBatchedRequest({ mints: mints.slice(counter, counter + batchSize) }),
),
);
for (let i = counter; i < Math.min(counter + batchSize, mints.length); i++) {
decimalPromises.push(connection.getParsedAccountInfo(new PublicKey(mints[i])));
}
counter += batchSize;
}
let allData: any[] = [];
try {
allData = await Promise.all([...decimalPromises, ...pricePromises]);
} catch (e) {
console.error("Failed to getParsedAccountInfo:", e);
throw e;
}
let decimalData = allData.slice(0, mints.length);
let mintDecimals: Record = {};
decimalData.forEach((accountInfo, index) => {
mintDecimals[mints[index]] = accountInfo!.value!.data?.parsed?.info?.decimals;
});
let priceData = allData.slice(mints.length);
let tokenTotal = 0;
for (const mintPriceData of priceData) {
for (const price of (mintPriceData as any).data) {
if (price.price) {
let decimal = mintDecimals[price.mints];
mintPrice[price.mints] = price.price / 1e6;
mintVolume[price.mints] = price.volume;
tokenTotal += (mintPrice[price.mints] * mintAmount[price.mints]) / Math.pow(10, decimal);
}
}
}
return (Number(tokenTotal * 100).toFixed(1) as any as number) / 100;
}
async function getNFTTotal(address: string) {
let assets = ((await readApiGetAssetsByOwner(address, 1, 100)) as any).items;
assets = assets.filter((asset: any) => asset.grouping.length > 0);
let groupings: Record = {};
for (const asset of assets) {
if (asset.grouping.length === 0) {
continue;
}
let grouping = asset.grouping[0];
let collection = grouping.group_value;
groupings[collection] = (groupings[collection] || 0) + 1;
}
let assetIds = assets.map((asset: any) => asset.id);
let nftPricePromises = assetIds.map((id: string) => {
return HELLOMOON_CLIENT.send(
new NftMintPriceByCreatorAvgRequest({
nftMint: id,
}),
).then(price => price.data[0]?.avg_usd_price ?? 0);
});
let prices = await Promise.all(nftPricePromises);
let nftTotal: number = prices.reduce((a, b) => a + b, 0);
return (Number(nftTotal * 100).toFixed(1) as any as number) / 100;
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
const address = req.body["address"];
let totals = await Promise.all([
getTokenTotal(address.toString()),
getNFTTotal(address.toString()),
]);
let [tokenTotal, nftTotal] = totals;
res.status(200).json({ tokenTotal, nftTotal, total: tokenTotal + nftTotal });
}
export default makeApiPostRequest(handler, { addresses: ["address"] });
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/getTransaction/index.ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import configConstants, { CONNECTION } from "../../constants";
configConstants();
import * as anchor from "@coral-xyz/anchor";
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import { stringifyAnchorObject } from "../getAccountInfo";
import {
AccountMeta,
ComputeBudgetInstruction,
ComputeBudgetInstructionType,
ComputeBudgetProgram,
LAMPORTS_PER_SOL,
SystemInstruction,
SystemProgram,
TransactionError,
TransactionInstruction,
} from "@solana/web3.js";
import {
TOKEN_PROGRAM_ID,
TOKEN_2022_PROGRAM_ID,
decodeInstruction,
TokenInstruction,
} from "@solana/spl-token";
import { u8 } from "@solana/buffer-layout";
// Setup Solflare
import { Client } from "@solflare-wallet/utl-sdk";
import { makeApiPostRequest } from "@/lib/middleware";
const utl = new Client();
type IxArgs = Record;
type Instruction = {
programId: string;
keys: string[];
ixData: string | IxArgs;
depth: number;
};
let PROGRAM_CACHE = new Map();
type ParsedTxResponse = {
// TODO(ngundotra): I'm pretty sure amount needs to be a string bc u57 overflow, but this is what SPL Token uses
tokenBalanceChanges: Record;
solBalanceChanges: Record;
instructions: Instruction[];
success: boolean;
timestamp: string;
feePayer: string;
err?: TransactionError;
// Dev Mode Only params
computeUnitsConsumed?: number;
txFeeInLamports?: number;
txVersion?: string;
slot?: number;
logs?: string[];
};
async function getAnchorProgram(programId: string) {
if (PROGRAM_CACHE.has(programId)) {
return PROGRAM_CACHE.get(programId);
}
try {
const program = await anchor.Program.at(
programId,
new anchor.AnchorProvider(CONNECTION, new NodeWallet(anchor.web3.Keypair.generate()), {}),
);
PROGRAM_CACHE.set(programId, program);
return program;
} catch (err) {
PROGRAM_CACHE.set(programId, null);
return null;
}
}
async function parseAnchorIxData(programId: anchor.web3.PublicKey, ixData: string) {
const program = await getAnchorProgram(programId.toString());
if (!program) {
return null;
}
let ixCoder = new anchor.BorshInstructionCoder(program.idl);
return ixCoder.decode(Buffer.from(ixData, "base64"));
}
async function parseAnchorIxAccounts(
programAddress: string,
accounts: string[],
ix: anchor.Instruction,
) {
const program = await getAnchorProgram(programAddress);
if (!program) {
return null;
}
const ixDef = program?.idl.instructions.find((ixDef: any) => ixDef.name === ix.name);
if (ixDef) {
let parsedAccounts = ixDef.accounts as {
// type coercing since anchor doesn't export the underlying type
name: string;
isMut: boolean;
isSigner: boolean;
pda?: object;
}[];
let allAccounts = parsedAccounts.map((acct, idx) => {
return JSON.stringify({
name: acct.name,
isMut: acct.isMut,
isSigner: acct.isSigner,
address: accounts[idx],
});
});
if (parsedAccounts && parsedAccounts.length < accounts.length) {
allAccounts = allAccounts.concat(accounts.slice(parsedAccounts.length));
}
return allAccounts;
}
return null;
}
export function snakeToTitleCase(str: string): string {
const result = str.replace(/([-_]\w)/g, g => ` ${g[1].toUpperCase()}`);
return result.charAt(0).toUpperCase() + result.slice(1);
}
async function parseAnchorIx(ix: Instruction) {
let parsedIx = await parseAnchorIxData(
new anchor.web3.PublicKey(ix.programId),
ix.ixData as string,
);
if (!parsedIx) {
return ix;
}
let parsedAccounts = await parseAnchorIxAccounts(ix.programId, ix.keys, parsedIx);
let ixTitle = parsedIx.name;
ixTitle = ixTitle.charAt(0).toUpperCase() + ixTitle.slice(1);
let program = await getAnchorProgram(ix.programId);
return {
programId: `${snakeToTitleCase(program!.idl.name)} (${ix.programId})`,
ixData: `${ixTitle} ${JSON.stringify(stringifyAnchorObject(parsedIx.data))}`,
keys: parsedAccounts ?? ix.keys,
depth: ix.depth,
};
}
async function parseIx(ix: TransactionInstruction, depth: number): Promise {
let programAddress = ix.programId.toBase58();
let parsedIx: Instruction = {
programId: programAddress,
keys: ix.keys.map(k => k.pubkey.toBase58()),
ixData: "",
depth,
};
if (programAddress === SystemProgram.programId.toBase58()) {
parsedIx.programId = "System Program";
let type = SystemInstruction.decodeInstructionType(ix);
switch (type) {
case "AdvanceNonceAccount":
parsedIx.ixData = stringifyAnchorObject({
AdvanceNonceAccount: SystemInstruction.decodeNonceAdvance(ix),
});
break;
case "Allocate":
parsedIx.ixData = stringifyAnchorObject({
Allocate: SystemInstruction.decodeAllocate(ix),
});
break;
case "AllocateWithSeed":
parsedIx.ixData = stringifyAnchorObject({
AllocateWithSeed: SystemInstruction.decodeAllocateWithSeed(ix),
});
break;
case "Assign":
parsedIx.ixData = stringifyAnchorObject({ Assign: SystemInstruction.decodeAssign(ix) });
break;
case "AssignWithSeed":
parsedIx.ixData = stringifyAnchorObject({
AssignWithSeed: SystemInstruction.decodeAssignWithSeed(ix),
});
break;
case "AuthorizeNonceAccount":
parsedIx.ixData = stringifyAnchorObject({
AuthorizeNonceAccount: SystemInstruction.decodeNonceAuthorize(ix),
});
break;
case "Create":
parsedIx.ixData = stringifyAnchorObject({
Create: SystemInstruction.decodeCreateAccount(ix),
});
break;
case "CreateWithSeed":
parsedIx.ixData = stringifyAnchorObject({
CreateWithSeed: SystemInstruction.decodeCreateWithSeed(ix),
});
break;
case "InitializeNonceAccount":
parsedIx.ixData = stringifyAnchorObject({
InitializeNonceAccount: SystemInstruction.decodeNonceInitialize(ix),
});
break;
case "Transfer":
parsedIx.ixData = stringifyAnchorObject({ Transfer: SystemInstruction.decodeTransfer(ix) });
break;
case "TransferWithSeed":
parsedIx.ixData = stringifyAnchorObject({
TransferWithSeed: SystemInstruction.decodeTransferWithSeed(ix),
});
break;
case "WithdrawNonceAccount":
parsedIx.ixData = stringifyAnchorObject({
WithdrawNonceAccount: SystemInstruction.decodeNonceWithdraw(ix),
});
break;
case "UpgradeNonceAccount":
break;
}
} else if (programAddress === TOKEN_2022_PROGRAM_ID.toBase58()) {
parsedIx.programId = "SPL Token Program 2022";
parsedIx.ixData = stringifyAnchorObject(await parseTokenInstruction(ix));
} else if (programAddress === TOKEN_PROGRAM_ID.toBase58()) {
parsedIx.programId = "SPL Token Program";
parsedIx.ixData = stringifyAnchorObject(await parseTokenInstruction(ix));
} else if (programAddress === ComputeBudgetProgram.programId.toBase58()) {
parsedIx.programId = "Compute Budget Program";
let type: ComputeBudgetInstructionType = ComputeBudgetInstruction.decodeInstructionType(ix);
switch (type) {
case "RequestHeapFrame":
parsedIx.ixData = stringifyAnchorObject({
RequestHeapFrame: ComputeBudgetInstruction.decodeRequestHeapFrame(ix),
});
break;
case "RequestUnits":
parsedIx.ixData = stringifyAnchorObject({
RequestUnits: ComputeBudgetInstruction.decodeRequestUnits(ix),
});
break;
case "SetComputeUnitLimit":
parsedIx.ixData = stringifyAnchorObject({
SetComputeUnitLimit: ComputeBudgetInstruction.decodeSetComputeUnitLimit(ix),
});
break;
case "SetComputeUnitPrice":
parsedIx.ixData = stringifyAnchorObject({
SetComputeUnit: ComputeBudgetInstruction.decodeSetComputeUnitPrice(ix),
});
break;
}
} else {
return await parseAnchorIx({
programId: programAddress,
ixData: anchor.utils.bytes.base64.encode(ix.data),
keys: ix.keys.map(k => k.pubkey.toBase58()),
depth,
});
}
return parsedIx;
}
/**
* Flattens `keys` to just be the name of the account and the address
* Extends `mint` addresses with the mint name (e.g. "(USDC)")
*/
async function parseTokenInstruction(ix: TransactionInstruction): Promise> {
let ixTypeByte = u8().decode(ix.data.slice(0, 1));
let ixName = TokenInstruction[ixTypeByte];
let decoded: Record = {};
let decodedIx = decodeInstruction(ix);
let keys: Record = {};
for (const keyName of Object.keys(decodedIx.keys)) {
const meta = (decodedIx.keys as Record)[keyName];
if (keyName === "multiSigners") {
continue;
}
let address = meta.pubkey.toBase58();
if (keyName === "mint") {
let mintData = await utl.fetchMint(meta.pubkey);
address = `${mintData.symbol}`;
}
keys[keyName] = address;
}
// @ts-ignore
decodedIx.keys = keys;
if (decodedIx.data && "amount" in decodedIx.data) {
let amount: anchor.BN;
if (typeof decodedIx.data.amount === "bigint") {
amount = new anchor.BN(decodedIx.data.amount.toString());
} else {
amount = new anchor.BN(decodedIx.data.amount);
}
// Todo(ngundotra): add support for decimals
if ("decimals" in decodedIx.data) {
let decimals = decodedIx.data.decimals;
amount = amount.div(new anchor.BN(10).pow(new anchor.BN(decimals)));
// @ts-ignore
delete decodedIx.data["decimals"];
}
// @ts-ignore
decodedIx.data.amount = amount;
}
decoded[ixName] = decodedIx;
return decoded;
}
async function parseTokenChanges(
preBalances: anchor.web3.ConfirmedTransactionMeta["preTokenBalances"],
postBalances: anchor.web3.ConfirmedTransactionMeta["postTokenBalances"],
) {
let mints = new Set([...preBalances!.map(b => b.mint), ...postBalances!.map(b => b.mint)]);
let mintData = await utl.fetchMints(
Array.from(mints.keys()).map(k => new anchor.web3.PublicKey(k)),
);
let mintMap: Record = {};
mintData
.filter(token => token.verified ?? true)
.forEach(m => {
mintMap[m.address.toString()] = m.symbol;
});
if (!postBalances || !preBalances || postBalances.length != preBalances.length) {
return null;
}
let changes: Record = {};
for (let i = 0; i < postBalances?.length; i++) {
let pre = preBalances[i];
let post = postBalances[i];
let tokenChange = (post.uiTokenAmount.uiAmount ?? 0) - (pre.uiTokenAmount.uiAmount ?? 0);
if (tokenChange !== 0) {
changes[pre.owner!] = { mint: mintMap[pre.mint] ?? pre.mint, amount: tokenChange };
}
}
return changes;
}
function parseSolChanges(
accounts: anchor.web3.PublicKey[],
preBalances: number[],
postBalances: number[],
) {
if (preBalances.length != postBalances.length) {
return null;
}
const changes: Record = {};
for (let i = 0; i < preBalances.length; i++) {
let pre = preBalances[i];
let post = postBalances[i];
let solChange = post - pre;
if (solChange > 0.0000000001) {
changes[accounts[i].toString()] = solChange / LAMPORTS_PER_SOL;
}
}
return changes;
}
function parseLogs(logs: string[]): { programId: string; depth: number }[] {
let traces: { programId: string; depth: number }[] = [];
for (const log of logs) {
let match = log.match(/Program (.*) invoke \[(.*)\]/);
if (match) {
let program = match[1];
let depth = match[2];
traces.push({ programId: program, depth: Number.parseInt(depth) });
}
}
return traces;
}
// TODO(ngundotra): add support for System program + SPL programs
async function handler(req: NextApiRequest, res: NextApiResponse) {
const signature = req.body.signature;
const devMode = req.body.devMode ?? false;
const transaction = await CONNECTION.getTransaction(signature, {
maxSupportedTransactionVersion: 2,
});
if (!transaction) {
res.status(404).send("Transaction not found");
}
let instructions: Instruction[] = [];
// Reconstruct account array
let accounts = transaction?.transaction.message.staticAccountKeys ?? [];
accounts = accounts.concat(transaction?.meta?.loadedAddresses?.readonly ?? []);
accounts = accounts.concat(transaction?.meta?.loadedAddresses?.writable ?? []);
let tokenBalanceChanges = await parseTokenChanges(
transaction?.meta?.preTokenBalances,
transaction?.meta?.postTokenBalances,
);
let solChanges = parseSolChanges(
accounts,
transaction?.meta?.preBalances ?? [],
transaction?.meta?.postBalances ?? [],
);
// Keep track of order of instructions (may not be able get stack depth for each inner ix because of log truncation)
let traceIdx = 0;
let parsedTrace = parseLogs(transaction?.meta?.logMessages ?? []);
// Loop through top-level instructions
let outerIdx = 0;
let innerIdx = 0;
for (const outerIx of transaction?.transaction.message.compiledInstructions ?? []) {
const programId = accounts[outerIx.programIdIndex];
instructions.push(
await parseIx(
{
programId,
data: Buffer.from(outerIx.data ?? []),
keys: outerIx.accountKeyIndexes.map(idx => {
return {
pubkey: accounts[idx],
isSigner: false,
isWritable: false,
};
}),
},
0,
),
);
traceIdx += 1;
// Loop through inner instructions
let innerIxBucket = transaction?.meta?.innerInstructions?.[innerIdx];
if (innerIxBucket && innerIxBucket.index === outerIdx) {
for (const innerIx of innerIxBucket.instructions) {
if (parsedTrace[traceIdx].programId !== accounts[innerIx.programIdIndex].toBase58()) {
// Make a note that the depth is unknown (somehow)
parsedTrace[traceIdx].depth = 1;
}
instructions.push(
await parseIx(
{
programId: accounts[innerIx.programIdIndex],
keys: innerIx.accounts.map(key => {
return {
pubkey: accounts[key],
isSigner: false,
isWritable: false,
};
}),
data: anchor.utils.bytes.bs58.decode(innerIx.data),
},
parsedTrace[traceIdx].depth,
),
);
traceIdx += 1;
}
innerIdx += 1;
}
outerIdx += 1;
}
const feePayer = transaction?.transaction.message.staticAccountKeys[0]!.toBase58();
let timestamp: Date | undefined;
if (transaction?.blockTime) {
timestamp = new Date();
timestamp.setTime(transaction?.blockTime! * 1000);
}
let response: ParsedTxResponse = {
err: transaction?.meta?.err ?? undefined,
feePayer: feePayer!,
instructions,
tokenBalanceChanges: tokenBalanceChanges!,
solBalanceChanges: solChanges!,
success: transaction?.meta?.err === null,
timestamp: timestamp?.toISOString() ?? "unknown",
};
if (devMode) {
response = {
...response,
computeUnitsConsumed: transaction?.meta?.computeUnitsConsumed!,
txFeeInLamports: transaction?.meta?.fee!,
txVersion: transaction?.version?.toString() ?? "legacy",
logs: transaction?.meta?.logMessages ?? [],
slot: transaction?.slot!,
};
}
res.status(200).send(JSON.stringify(response));
}
export default makeApiPostRequest(handler);
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/solana-pay/page/[methodName].ts
================================================
/// DEPRECATED - NO LONGER IN USE
import { NextApiRequest, NextApiResponse } from "next";
import { encode } from "querystring";
import configConstants, { SELF_URL, TX_DESCRIPTIONS } from "../../../constants";
configConstants();
function createOpenGraphMetaPage(
methodName: string,
encoded: string,
description: string
): string {
let qrCodeUri = new URL(
`${SELF_URL}/api/handlers/solana-pay/qr/${methodName}?${encoded}`
);
return `
`;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const {
query: { methodName },
} = req;
console.log("OpenGraph metapage requested:", methodName, req.query);
let description = TX_DESCRIPTIONS[methodName as string];
res
.status(200)
.send(
createOpenGraphMetaPage(
methodName as string,
encode(Object(req.query)),
description
)
);
}
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/solana-pay/qr/[methodName].ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import { encode } from "querystring";
import { encodeURL } from "@solana/pay";
import * as qrcode from "qrcode";
import sharp from "sharp";
import configConstants, {
SELF_URL,
SOLANA_PAY_LABEL,
} from "../../../constants";
configConstants();
async function createQRCodePng(
methodName: string,
encoded: string
): Promise {
let uri = new URL(
`${SELF_URL}/api/handlers/solana-pay/sign/${methodName}?${encoded}`
);
let solanaPayUrl = encodeURL({
link: uri,
label: SOLANA_PAY_LABEL,
});
let dataUrl = await qrcode.toDataURL(solanaPayUrl.toString(), {
errorCorrectionLevel: "H",
});
const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
const imageBuffer = Buffer.from(base64Data, "base64");
return await sharp(imageBuffer)
.extend({
extendWith: "background",
background: "#ffffff",
left: 10,
right: 10,
})
.toBuffer();
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const {
query: { methodName },
} = req;
console.log("QR code requested:", methodName, req.query);
let buffer = await createQRCodePng(
methodName as string,
encode(Object(req.query))
);
res.setHeader("Content-Type", "image/png");
res.setHeader("Content-Disposition", "inline");
res.status(200).send(buffer);
}
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/solana-pay/sign/createBurnAsset.ts
================================================
// DEPRECATED - not in use
import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes";
import { createBurnInstruction } from "@metaplex-foundation/mpl-bubblegum";
import {
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
SPL_NOOP_PROGRAM_ID,
} from "@solana/spl-account-compression";
import { PublicKey, Transaction } from "@solana/web3.js";
import { NextApiRequest } from "next";
import { makeRespondToSolanaPayGet, makeRespondToSolanaPayPost } from ".";
import configConstants, { CONNECTION } from "../../../constants";
import { bufferToArray, getAsset, getAssetProof, getBubblegumAuthorityPDA } from "../utils/helpers";
configConstants();
export async function createBurnAsset(req: NextApiRequest) {
const { assetId } = req.query;
const { account: owner } = req.body;
if (!owner || !assetId) {
throw new Error("Missing required parameters");
}
let assetProof = await getAssetProof(assetId, CONNECTION.rpcEndpoint);
const rpcAsset = await getAsset(assetId, CONNECTION.rpcEndpoint);
const leafNonce = rpcAsset.compression.leaf_id;
let proofPath = assetProof.proof.map((node: string) => ({
pubkey: new PublicKey(node),
isSigner: false,
isWritable: false,
}));
const treeAuthority = await getBubblegumAuthorityPDA(new PublicKey(assetProof.tree_id));
const leafDelegate = rpcAsset.ownership.delegate
? new PublicKey(rpcAsset.ownership.delegate)
: new PublicKey(rpcAsset.ownership.owner);
if (rpcAsset.ownership.owner !== owner) {
throw new Error(
`NFT is not owned by the expected owner. Expected ${new PublicKey(owner)} but got ${
rpcAsset.ownership.owner
}.`,
);
}
const burnIx = createBurnInstruction(
{
treeAuthority,
leafOwner: new PublicKey(rpcAsset.ownership.owner),
leafDelegate,
merkleTree: new PublicKey(assetProof.tree_id),
logWrapper: SPL_NOOP_PROGRAM_ID,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
anchorRemainingAccounts: proofPath,
},
{
root: bufferToArray(bs58.decode(assetProof.root)),
dataHash: bufferToArray(bs58.decode(rpcAsset.compression.data_hash.trim())),
creatorHash: bufferToArray(bs58.decode(rpcAsset.compression.creator_hash.trim())),
nonce: leafNonce,
index: leafNonce,
},
);
const tx = new Transaction().add(burnIx);
tx.feePayer = new PublicKey(owner);
tx.recentBlockhash = (await CONNECTION.getLatestBlockhash()).blockhash;
return {
transaction: tx.serialize({ requireAllSignatures: false }).toString("base64"),
};
}
export default makeRespondToSolanaPayGet(makeRespondToSolanaPayPost(createBurnAsset));
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/solana-pay/sign/createBuyNFT.ts
================================================
// DEPRECATED - not in use
import { NextApiRequest } from "next";
import { base64 } from "@coral-xyz/anchor/dist/cjs/utils/bytes";
import configConstants, { HYPERSPACE_CLIENT } from "../../../constants";
configConstants();
import { makeRespondToSolanaPayPost, makeRespondToSolanaPayGet } from ".";
async function hyperspaceCreateBuyTx(buyer: string, token: string, price: number) {
let transactionData = await HYPERSPACE_CLIENT.createBuyTx({
buyerAddress: buyer,
tokenAddress: token,
price: price,
// Take no fee on making tx for ChatGPT users
buyerBroker: "",
buyerBrokerBasisPoints: 0,
});
const txBytes = base64.encode(Buffer.from(transactionData.createBuyTx.stdBuffer!));
return {
transaction: txBytes,
};
}
export async function createBuyNFT(req: NextApiRequest) {
const { token, price } = req.query;
const { account: buyer } = req.body;
return await hyperspaceCreateBuyTx(
buyer as string,
token as string,
Number.parseFloat(price as string),
);
}
export default makeRespondToSolanaPayGet(makeRespondToSolanaPayPost(createBuyNFT));
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/solana-pay/sign/createCloseNFTMetadata.ts
================================================
// DEPRECATED - not in use
import { NextApiRequest } from "next";
import { createCloseNFTMetadataTx } from "@/lib/on-chain-metadata";
import { CONNECTION } from "../../../constants";
import { makeRespondToSolanaPayGet, makeRespondToSolanaPayPost } from ".";
async function createCloseNFTMetadata(req: NextApiRequest) {
const { account } = req.query;
const { account: owner } = req.body;
return await createCloseNFTMetadataTx(CONNECTION, owner as string, account as string);
}
export default makeRespondToSolanaPayGet(makeRespondToSolanaPayPost(createCloseNFTMetadata));
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/solana-pay/sign/createListNFT.ts
================================================
// DEPRECATED - not in use
/**
* Deprecated because Solana Pay compatible wallets block NFT/token delegation to prevent scams
* This means we can't list via Solana Pay, but maybe we can get around this by using NFT AMMs
* to sell at the floor.
*/
import { NextApiRequest } from "next";
import configConstants, { CONNECTION } from "../../../constants";
configConstants();
import { makeRespondToSolanaPayPost, makeRespondToSolanaPayGet } from ".";
import { AnchorProvider, BN, Program } from "@coral-xyz/anchor";
import { Hyperspace, hyperspaceIdl } from "@/lib/hyperspace/idl/hyperspace";
import {
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
SYSVAR_CLOCK_PUBKEY,
SYSVAR_INSTRUCTIONS_PUBKEY,
SystemProgram,
Transaction,
TransactionInstruction,
} from "@solana/web3.js";
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import {
findTokenRecordPda,
getAtaForMint,
getEditionDataAccount,
getHyperspaceProgramAsSigner,
getHyperspaceTradeState,
getMetadata,
} from "@/lib/hyperspace/account";
import { TOKEN_PROGRAM_ID, getAccount } from "@solana/spl-token";
import {
Metadata,
PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID,
} from "@metaplex-foundation/mpl-token-metadata";
import { HYPERSPACE_ID, HYPERSPACE_MARKETPLACE_INSTANCE } from "@/lib/hyperspace/constants";
const AUTH_PROGRAM_ID = new PublicKey("auth9SigNpDKz4sJJ1DfCTuZrZNSAgh9sFD3rboVmgg");
// async function hyperspaceCreateListTx(
// seller: string,
// token: string,
// price: number
// ) {
// console.log(seller, token, price);
// let transactionData = await HYPERSPACE_CLIENT.createListTx({
// sellerAddress: seller,
// tokenAddress: token,
// price: price,
// // Take no fee on making tx for ChatGPT users
// sellerBroker: "",
// sellerBrokerBasisPoints: 0,
// });
// console.log(transactionData);
// const txBytes = base64.encode(
// Buffer.from(transactionData.createListTx.stdBuffer!)
// );
// console.log(txBytes);
// return {
// transaction: txBytes,
// };
// }
export const getPriceWithMantissa = async (
price: number,
mint: PublicKey,
walletKeyPair: any,
anchorProgram: Program,
): Promise => {
// // const token = new Token(
// // anchorProgram.provider.connection,
// // new PublicKey(mint),
// // TOKEN_PROGRAM_ID,
// // walletKeyPair
// // );
// let ata = getAssociatedTokenAddressSync(mint, walletKeypair);
// const mintInfo = await token.getMintInfo();
// const mantissa = 10 ** mintInfo.decimals;
// return Math.ceil(price * mantissa);
return 1;
};
async function createTradeStateInstruction(
anchorProgram: Program,
isBuy: boolean,
tradeBump: number,
buyPrice: BN,
brokerBasisPoints: number,
royalty_basis_points = 0,
tokenSize: BN,
user: PublicKey,
broker: PublicKey,
tokenAccount: PublicKey,
tokenMint: PublicKey,
tradeStateAddress: PublicKey,
): Promise {
console.log(`Create instruction for creating trade state with address ${tradeStateAddress}
for ${user.toBase58()} (broker: ${broker.toBase58()}, basis points: ${brokerBasisPoints})
for token ${tokenMint.toBase58()} at price ${buyPrice}`);
let instruction = anchorProgram.instruction.createTradeState(
isBuy ? 1 : 0,
tradeBump,
buyPrice,
brokerBasisPoints,
tokenSize,
royalty_basis_points,
{
accounts: {
wallet: user,
collection: HYPERSPACE_ID,
brokerWallet: broker,
tokenAccount: tokenAccount,
tokenMint: tokenMint,
tradeState: tradeStateAddress,
systemProgram: SystemProgram.programId,
clock: SYSVAR_CLOCK_PUBKEY,
},
},
);
instruction.keys.filter(k => k.pubkey.equals(user)).map(k => (k.isSigner = true));
return instruction;
}
async function helper(
anchorProgram: Program,
seller: PublicKey,
mintPublicKey: PublicKey,
sellerBrokerKey: PublicKey,
minAmountToReceive: BN,
// We don't charge for creating this
brokerBasisPoints: number = 0,
// This only matters when you cross
royaltyBasisPoints: number = 0,
) {
const tokenSizeAdjusted = new BN(
await getPriceWithMantissa(1, mintPublicKey, seller, anchorProgram),
);
const tokenAccountKey = (await getAtaForMint(mintPublicKey, seller))[0];
// You should check that ATA exists
let instructions: TransactionInstruction[] = [];
const [programAsSigner, programAsSignerBump] = await getHyperspaceProgramAsSigner();
const [tradeState, tradeBump] = await getHyperspaceTradeState(
false,
seller,
tokenAccountKey,
mintPublicKey,
tokenSizeAdjusted,
);
const tradeStateAccount = await anchorProgram.provider.connection.getAccountInfo(
tradeState,
"confirmed",
);
if (!tradeStateAccount) {
const initTradeStateInstruction = await createTradeStateInstruction(
anchorProgram,
false,
tradeBump,
minAmountToReceive,
brokerBasisPoints,
royaltyBasisPoints,
tokenSizeAdjusted,
seller,
sellerBrokerKey,
tokenAccountKey,
mintPublicKey,
tradeState,
);
instructions.push(initTradeStateInstruction);
}
const tokenRecord = findTokenRecordPda(mintPublicKey, tokenAccountKey);
const editionAccount = (await getEditionDataAccount(mintPublicKey))[0];
const metadataAccount = await getMetadata(mintPublicKey);
const metadataObj = await anchorProgram.provider.connection.getAccountInfo(
metadataAccount,
"confirmed",
);
if (!metadataObj) {
throw Error("NFT does not have a metadata account, it may have been burnt.");
}
const metadataParsed = Metadata.deserialize(metadataObj.data)[0];
const signers: Keypair[] = [];
let marketplaceObj = await anchorProgram.account.hyperspace.fetch(
HYPERSPACE_MARKETPLACE_INSTANCE,
);
const sellInstruction = await anchorProgram.instruction.sell(
tradeBump,
programAsSignerBump,
minAmountToReceive,
brokerBasisPoints,
tokenSizeAdjusted,
royaltyBasisPoints,
{
accounts: {
wallet: seller,
sellerBrokerWallet: sellerBrokerKey,
tokenMint: mintPublicKey,
tokenAccount: tokenAccountKey,
metadata: metadataAccount,
authority: marketplaceObj.authority,
hyperspace: HYPERSPACE_MARKETPLACE_INSTANCE,
hyperspaceFeeAccount: marketplaceObj.hyperspaceFeeAccount,
sellerTradeState: tradeState,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
metadataProgram: TOKEN_METADATA_PROGRAM_ID,
programAsSigner: programAsSigner,
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
tokenRecord: tokenRecord,
editionAccount: editionAccount,
authorizationRules: metadataParsed.programmableConfig?.ruleSet
? metadataParsed.programmableConfig.ruleSet
: TOKEN_METADATA_PROGRAM_ID,
mplTokenAuthRulesProgram: AUTH_PROGRAM_ID,
clock: SYSVAR_CLOCK_PUBKEY,
},
signers,
},
);
instructions.push(sellInstruction);
return instructions;
}
async function hyperspaceCreateListTx(seller: string, token: string, price: number) {
let provider = new AnchorProvider(CONNECTION, new NodeWallet(Keypair.generate()), {});
let program = new Program(hyperspaceIdl, HYPERSPACE_ID, provider);
const sellerKey = new PublicKey(seller);
const tokenKey = new PublicKey(token);
let mint: PublicKey;
try {
const tokenAI = await getAccount(CONNECTION, tokenKey);
console.log("found tokenAI:", tokenAI);
mint = tokenAI.mint;
} catch (e) {
mint = tokenKey;
}
let instructions = await helper(
program,
sellerKey,
mint,
SystemProgram.programId,
new BN(price * LAMPORTS_PER_SOL),
);
let tx = new Transaction();
for (const ix of instructions) {
tx = tx.add(ix);
}
tx.recentBlockhash = (await CONNECTION.getLatestBlockhash()).blockhash;
tx.feePayer = sellerKey;
const txBytes = tx.serialize({ requireAllSignatures: false }).toString("base64");
return {
transaction: txBytes,
};
}
export async function createListNFT(req: NextApiRequest) {
const { token, price } = req.query;
const { account: seller } = req.body;
console.log(seller, token, price);
return await hyperspaceCreateListTx(
seller as string,
token as string,
Number.parseFloat(price as string),
);
}
export default makeRespondToSolanaPayGet(makeRespondToSolanaPayPost(createListNFT));
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/solana-pay/sign/createMerkleTree.ts
================================================
// DEPRECATED - not in use
import {
PROGRAM_ID as BUBBLEGUM_PROGRAM_ID,
createCreateTreeInstruction,
} from "@metaplex-foundation/mpl-bubblegum";
import {
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
SPL_NOOP_PROGRAM_ID,
createAllocTreeIx,
} from "@solana/spl-account-compression";
import { Keypair, PublicKey, Transaction } from "@solana/web3.js";
import dotenv from "dotenv";
import { NextApiRequest } from "next";
import { CONNECTION } from "../../../constants";
dotenv.config();
//TODO(@ngundotra): have to add closeTree instruction before exposing this endpoint
export interface MerkleTreeArgs {
maxDepth: number;
maxBufferSize: number;
canopyHeight: number;
}
export async function createTree(req: NextApiRequest) {
const { account: payerPublicKey, canopyDepth, maxDepthSizePair } = req.body;
const treeKeypair = Keypair.generate();
const [treeAuthority, _bump] = PublicKey.findProgramAddressSync(
[treeKeypair.publicKey.toBuffer()],
BUBBLEGUM_PROGRAM_ID,
);
// instruction for space allocation for tree account
const allocTreeIx = await createAllocTreeIx(
CONNECTION,
treeKeypair.publicKey,
payerPublicKey,
maxDepthSizePair,
canopyDepth,
);
// instruction for tree creation
const createTreeIx = createCreateTreeInstruction(
{
payer: payerPublicKey,
treeCreator: payerPublicKey,
treeAuthority: treeAuthority,
merkleTree: treeKeypair.publicKey,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
logWrapper: SPL_NOOP_PROGRAM_ID,
},
{
maxBufferSize: maxDepthSizePair.maxBufferSize,
maxDepth: maxDepthSizePair.maxDepth,
public: false,
},
BUBBLEGUM_PROGRAM_ID,
);
try {
const tx = new Transaction();
tx.add(allocTreeIx);
tx.add(createTreeIx);
tx.feePayer = payerPublicKey;
return tx;
} catch (e) {
console.error(e);
throw e;
}
}
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/solana-pay/sign/createMintCNFT.ts
================================================
// DEPRECATED - not in use
import { JsonMetadata, Metaplex } from "@metaplex-foundation/js";
import {
PROGRAM_ID as BUBBLEGUM_PROGRAM_ID,
MetadataArgs,
TokenProgramVersion,
TokenStandard,
createMintToCollectionV1Instruction,
} from "@metaplex-foundation/mpl-bubblegum";
import { PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID } from "@metaplex-foundation/mpl-token-metadata";
import {
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
SPL_NOOP_PROGRAM_ID,
} from "@solana/spl-account-compression";
import { Keypair, PublicKey, Transaction } from "@solana/web3.js";
import dotenv from "dotenv";
import { NextApiRequest } from "next";
import { makeRespondToSolanaPayGet, makeRespondToSolanaPayPost } from ".";
import configConstants, { CONNECTION } from "../../../constants";
configConstants();
dotenv.config();
async function createMintCNFT(req: NextApiRequest) {
const { metadataUri } = req.query;
const { account: payer } = req.body;
const metaplex = Metaplex.make(CONNECTION);
// create compressed nft
const nftMetadata = await metaplex.storage().downloadJson(metadataUri as string);
const compressedNftMetadata: MetadataArgs = {
name: nftMetadata.name ?? "",
symbol: nftMetadata.symbol ?? "",
uri: metadataUri as string,
sellerFeeBasisPoints: 0,
creators: [
{
address: new PublicKey(payer),
verified: true,
share: 100,
},
],
editionNonce: 0,
uses: null,
collection: null,
isMutable: false,
primarySaleHappened: false,
tokenProgramVersion: TokenProgramVersion.Original,
tokenStandard: TokenStandard.NonFungible,
};
// derive PDA (owned bt Bubblegum) to act as the signer of the compressed minting
const [bubblegumSigner, _bump] = PublicKey.findProgramAddressSync(
[Buffer.from("collection_cpi", "utf8")],
BUBBLEGUM_PROGRAM_ID,
);
const collectionOwner = Keypair.fromSecretKey(
Uint8Array.from(Buffer.from(process.env.COLLECTION_OWNER_SECRET_KEY as string, "base64")),
);
const mintToCollectionIx = createMintToCollectionV1Instruction(
{
payer: new PublicKey(payer),
merkleTree: new PublicKey(process.env.TREE_ADDRESS_1 as string),
treeAuthority: new PublicKey(process.env.TREE_AUTHORITY_1 as string),
treeDelegate: new PublicKey(payer),
collectionMint: new PublicKey(process.env.CHATGPT_COLLECTION_MINT as string),
collectionAuthority: collectionOwner.publicKey,
collectionMetadata: new PublicKey(process.env.CHATGPT_COLLECTION_METADATA_ACCOUNT as string),
collectionAuthorityRecordPda: BUBBLEGUM_PROGRAM_ID,
editionAccount: new PublicKey(
process.env.CHATGPT_COLLECTION_MASTER_EDITION_ACCOUNT as string,
),
leafOwner: new PublicKey(payer),
leafDelegate: new PublicKey(payer),
// other accounts
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
logWrapper: SPL_NOOP_PROGRAM_ID,
bubblegumSigner: bubblegumSigner,
tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
},
{
metadataArgs: Object.assign(compressedNftMetadata, {
collection: {
key: new PublicKey(process.env.CHATGPT_COLLECTION_MINT as string),
verified: false,
},
}),
},
);
const tx = new Transaction();
tx.add(mintToCollectionIx);
tx.feePayer = new PublicKey(payer);
tx.recentBlockhash = (await CONNECTION.getLatestBlockhash()).blockhash;
tx.partialSign(collectionOwner);
return {
transaction: tx.serialize({ requireAllSignatures: false }).toString("base64"),
};
}
export default makeRespondToSolanaPayGet(makeRespondToSolanaPayPost(createMintCNFT));
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/solana-pay/sign/createMintNFT.ts
================================================
// DEPRECATED - not in use
import { Metaplex } from "@metaplex-foundation/js";
import { PublicKey } from "@solana/web3.js";
import { NextApiRequest } from "next";
import { makeRespondToSolanaPayGet, makeRespondToSolanaPayPost } from ".";
import configConstants, { CONNECTION } from "../../../constants";
configConstants();
async function createMintNFT(req: NextApiRequest) {
const { name, metadataUri, sellerFee = 0 } = req.query;
const { account: sender } = req.body;
const metaplex = Metaplex.make(CONNECTION);
const mintTransactionBuilder = await metaplex
.nfts()
.builders()
.create({
name: name as string,
uri: metadataUri as string,
sellerFeeBasisPoints: Number(sellerFee),
});
const latestBlockhashWithExpiryBlockHeight = await CONNECTION.getLatestBlockhash();
const tx = mintTransactionBuilder.toTransaction(latestBlockhashWithExpiryBlockHeight);
tx.feePayer = new PublicKey(sender);
return {
transaction: tx.serialize({ requireAllSignatures: false }).toString("base64"),
};
}
export default makeRespondToSolanaPayGet(makeRespondToSolanaPayPost(createMintNFT));
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/solana-pay/sign/createSetProfilePic.ts
================================================
// DEPRECATED - not in use
import { NextApiRequest } from "next";
import { PublicKey } from "@solana/web3.js";
import { makeRespondToSolanaPayGet, makeRespondToSolanaPayPost } from ".";
import configConstants, { CONNECTION } from "../../../constants";
import { createSetProfilePictureTransaction } from "@solflare-wallet/pfp";
configConstants();
async function createSetProfilePic(req: NextApiRequest) {
const { mintPublicKey, tokenAccountPublicKey } = req.query;
const { account: ownerAccountPublicKey } = req.body;
const tx = await createSetProfilePictureTransaction(
new PublicKey(ownerAccountPublicKey),
new PublicKey(mintPublicKey as string),
new PublicKey(tokenAccountPublicKey as string),
);
tx.feePayer = new PublicKey(ownerAccountPublicKey);
tx.recentBlockhash = (await CONNECTION.getLatestBlockhash()).blockhash;
return {
transaction: tx.serialize({ requireAllSignatures: false }).toString("base64"),
};
}
export default makeRespondToSolanaPayGet(makeRespondToSolanaPayPost(createSetProfilePic));
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/solana-pay/sign/createTransferAsset.ts
================================================
// DEPRECATED - not in use
import {
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
SPL_NOOP_PROGRAM_ID,
} from "@solana/spl-account-compression";
import { PublicKey, Transaction } from "@solana/web3.js";
import { NextApiRequest } from "next";
import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes";
import { createTransferInstruction } from "@metaplex-foundation/mpl-bubblegum";
import { makeRespondToSolanaPayGet, makeRespondToSolanaPayPost } from ".";
import configConstants, { CONNECTION } from "../../../constants";
import { bufferToArray, getAsset, getAssetProof, getBubblegumAuthorityPDA } from "../utils/helpers";
configConstants();
async function createTransferAsset(req: NextApiRequest) {
const { destination, assetId } = req.query;
const { account: sender } = req.body;
if (!sender || !destination || !assetId) {
throw new Error("Missing required parameters");
}
const assetProof = await getAssetProof(assetId, CONNECTION.rpcEndpoint);
if (!assetProof?.proof || assetProof.proof.length === 0) {
throw new Error("Proof retrieved for the given assetId is empty. Please check the assetId.");
}
const proofPath = assetProof.proof.map((node: string) => ({
pubkey: new PublicKey(node),
isSigner: false,
isWritable: false,
}));
const rpcAsset = await getAsset(assetId, CONNECTION.rpcEndpoint);
if (rpcAsset.ownership.owner !== sender) {
throw new Error(
`NFT is not owned by the expected owner. Expected ${new PublicKey(sender)} but got ${
rpcAsset.ownership.owner
}.`,
);
}
const leafNonce = rpcAsset.compression.leaf_id;
const treeAuthority = await getBubblegumAuthorityPDA(new PublicKey(assetProof.tree_id));
const leafDelegate = rpcAsset.ownership.delegate
? new PublicKey(rpcAsset.ownership.delegate)
: new PublicKey(rpcAsset.ownership.owner);
let transferIx = createTransferInstruction(
{
treeAuthority,
leafOwner: new PublicKey(rpcAsset.ownership.owner),
leafDelegate: leafDelegate,
newLeafOwner: new PublicKey(destination as string),
merkleTree: new PublicKey(assetProof.tree_id),
logWrapper: SPL_NOOP_PROGRAM_ID,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
anchorRemainingAccounts: proofPath,
},
{
root: bufferToArray(bs58.decode(assetProof.root)),
dataHash: bufferToArray(bs58.decode(rpcAsset.compression.data_hash.trim())),
creatorHash: bufferToArray(bs58.decode(rpcAsset.compression.creator_hash.trim())),
nonce: leafNonce,
index: leafNonce,
},
);
const tx = new Transaction().add(transferIx);
tx.feePayer = new PublicKey(sender);
tx.recentBlockhash = (await CONNECTION.getLatestBlockhash()).blockhash;
return {
transaction: tx.serialize({ requireAllSignatures: false }).toString("base64"),
};
}
export default makeRespondToSolanaPayGet(makeRespondToSolanaPayPost(createTransferAsset));
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/solana-pay/sign/createTransferSol.ts
================================================
import { NextApiRequest } from "next";
import { PublicKey, Transaction, SystemProgram, LAMPORTS_PER_SOL } from "@solana/web3.js";
import { makeRespondToSolanaPayGet, makeRespondToSolanaPayPost } from ".";
import configConstants, { CONNECTION } from "../../../constants";
configConstants();
async function createTransferSol(req: NextApiRequest) {
const { amount } = req.query;
const { account } = req.body;
const sender = new PublicKey(account);
const destination = req.query["destination"] as any as PublicKey;
const tx = new Transaction();
tx.add(
SystemProgram.transfer({
fromPubkey: sender,
toPubkey: destination,
lamports: Math.floor(parseFloat(amount as string) * LAMPORTS_PER_SOL),
}),
);
tx.feePayer = sender;
tx.recentBlockhash = (await CONNECTION.getLatestBlockhash()).blockhash;
return {
transaction: tx.serialize({ requireAllSignatures: false }).toString("base64"),
};
}
export default makeRespondToSolanaPayGet(
makeRespondToSolanaPayPost(createTransferSol, { addresses: ["destination"] }),
);
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/solana-pay/sign/createTransferToken.ts
================================================
import { NextApiRequest } from "next";
import { PublicKey, Transaction } from "@solana/web3.js";
import {
getAssociatedTokenAddressSync,
createTransferCheckedInstruction,
getMint,
Mint,
} from "@solana/spl-token";
import { makeRespondToSolanaPayGet, makeRespondToSolanaPayPost } from ".";
import configConstants, { CONNECTION } from "../../../constants";
configConstants();
async function createTransferToken(req: NextApiRequest) {
const { amount } = req.query;
const { account } = req.body;
const sender = new PublicKey(account);
const destination = req.query["destination"] as any as PublicKey;
const mint = req.query["mint"] as any as PublicKey;
let mintAccount: Mint;
try {
mintAccount = await getMint(CONNECTION, mint, "confirmed");
} catch (error) {
throw new Error(`Mint ${mint.toString()} not found`);
}
const sourceToken = getAssociatedTokenAddressSync(mint, sender);
const destinationToken = getAssociatedTokenAddressSync(mint, destination);
const tx = new Transaction();
const tokens = Math.round(Number(amount as string) * Math.pow(10, mintAccount.decimals));
tx.add(
createTransferCheckedInstruction(
sourceToken,
mint,
destinationToken,
sender,
tokens,
mintAccount.decimals,
),
);
tx.feePayer = sender;
tx.recentBlockhash = (await CONNECTION.getLatestBlockhash()).blockhash;
return {
transaction: tx.serialize({ requireAllSignatures: false }).toString("base64"),
};
}
export default makeRespondToSolanaPayGet(
makeRespondToSolanaPayPost(createTransferToken, { addresses: ["destination"], tokens: ["mint"] }),
);
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/solana-pay/sign/createWriteNFTMetadata.ts
================================================
// DEPRECATED - not in use
import { createWriteNFTMetadataTx } from "@/lib/on-chain-metadata";
import { NextApiRequest } from "next";
import { makeRespondToSolanaPayGet, makeRespondToSolanaPayPost } from ".";
import configConstants, { CONNECTION } from "../../../constants";
configConstants();
async function createWriteNFTMetadata(req: NextApiRequest) {
const { image } = req.query;
const { account: owner } = req.body;
return await createWriteNFTMetadataTx(CONNECTION, owner as string, {
image,
});
}
export default makeRespondToSolanaPayGet(makeRespondToSolanaPayPost(createWriteNFTMetadata));
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/solana-pay/sign/index.ts
================================================
import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
import { SOLANA_PAY_LABEL } from "../../../constants";
import { Options, makeApiPostRequest } from "@/lib/middleware";
export type TransactionHandler = (req: NextApiRequest) => Promise<{ transaction: string }>;
export function makeRespondToSolanaPayGet(apiHandler: NextApiHandler) {
return async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === "GET") {
res.status(200).json({
label: SOLANA_PAY_LABEL,
icon: "https://solanapay.com/src/img/branding/Solanapay.com/downloads/gradient.svg",
});
} else {
await apiHandler(req, res);
}
};
}
export function makeRespondToSolanaPayPost(handler: TransactionHandler, options?: Options) {
return makeApiPostRequest(
async (req: NextApiRequest, res: NextApiResponse) => {
let result = await handler(req);
res.status(200).json(result);
},
{ ...options, rewriteQuery: true },
);
}
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/solana-pay/utils/helpers.ts
================================================
import { BN } from "@coral-xyz/anchor";
import { PROGRAM_ID, TreeConfig } from "@metaplex-foundation/mpl-bubblegum";
import { PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID } from "@metaplex-foundation/mpl-token-metadata";
import { Connection, PublicKey } from "@solana/web3.js";
import axios from "axios";
export async function getBubblegumAuthorityPDA(merkleRollPubKey: PublicKey) {
const [bubblegumAuthorityPDAKey] = await PublicKey.findProgramAddress(
[merkleRollPubKey.toBuffer()],
PROGRAM_ID,
);
return bubblegumAuthorityPDAKey;
}
export async function getNonceCount(connection: Connection, tree: PublicKey): Promise {
const treeAuthority = await getBubblegumAuthorityPDA(tree);
return new BN((await TreeConfig.fromAccountAddress(connection, treeAuthority)).numMinted);
}
export function bufferToArray(buffer: Buffer): number[] {
const nums = [];
for (let i = 0; i < buffer.length; i++) {
nums.push(buffer[i]);
}
return nums;
}
export async function getVoucherPDA(tree: PublicKey, leafIndex: number): Promise {
const [voucher] = await PublicKey.findProgramAddress(
[
Buffer.from("voucher", "utf8"),
tree.toBuffer(),
Uint8Array.from(new BN(leafIndex).toArray("le", 8)),
],
PROGRAM_ID,
);
return voucher;
}
export async function getMetadata(mint: PublicKey) {
return (
await PublicKey.findProgramAddress(
[Buffer.from("metadata"), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer()],
TOKEN_METADATA_PROGRAM_ID,
)
)[0];
}
export async function getMasterEdition(mint: PublicKey) {
return (
await PublicKey.findProgramAddress(
[
Buffer.from("metadata"),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
mint.toBuffer(),
Buffer.from("edition"),
],
TOKEN_METADATA_PROGRAM_ID,
)
)[0];
}
export async function getAsset(assetId: any, rpcUrl: any): Promise {
try {
const response = await axios.post(rpcUrl, {
jsonrpc: "2.0",
method: "get_asset",
id: "compression-example",
params: [assetId],
});
return response.data.result;
} catch (error) {
console.error(error);
}
}
export async function getAssetProof(assetId: any, rpcUrl: any): Promise {
try {
const response = await axios.post(rpcUrl, {
jsonrpc: "2.0",
method: "get_asset_proof",
id: "compression-example",
params: [assetId],
});
return response.data.result;
} catch (error) {
console.error(error);
}
}
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/tokenName/index.ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import { Client } from "@solflare-wallet/utl-sdk";
import { makeApiPostRequest } from "@/lib/middleware";
const utl = new Client();
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { tokenName } = req.body;
let results = await (
await utl.searchMints(tokenName)
)
.filter((res: any) => {
return res["verified"] && res["holders"] > 0 && res["chainId"] === 101;
})
.map((res: any) => {
return {
mintAddress: res["address"],
tokenName: res["name"],
tokenSymbol: res["symbol"],
holders: res["holders"],
verified: res["verified"],
chainId: res["chainId"],
logoURI: res["logoURI"],
};
});
res.status(200).json(results.slice(0, 10));
}
export default makeApiPostRequest(handler);
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/tx-link/[txSlug].ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import { encode } from "querystring";
import configConstants, { SELF_URL } from "../../constants";
configConstants();
import { makeApiPostRequest } from "@/lib/middleware";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const {
query: { txSlug },
} = req;
let encoded = encode(Object(req.body));
res.status(200).send({
qrCode: `${SELF_URL}/api/handlers/solana-pay/qr/${txSlug}?${encoded}`,
disclaimer: `This product uses artificial intelligence ("AI"), which may produce inaccurate information. You are responsible for transactions you authorize, so please confirm accuracy of instructions prior to authorizing any transaction.`,
});
}
export default makeApiPostRequest(handler);
================================================
FILE: chatgpt-plugin/src/pages/api/handlers/walletName/index.ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import { PublicKey } from "@solana/web3.js";
import { walletNameToAddressAndProfilePicture } from "@portal-payments/solana-wallet-names";
import { walletAddressToNameAndProfilePictureCustom } from "@/lib/address";
import configConstants, { CONNECTION } from "../../constants";
configConstants();
import { makeApiPostRequest } from "@/lib/middleware";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { walletName } = req.body;
try {
const dotAnything = await walletAddressToNameAndProfilePictureCustom(
CONNECTION,
new PublicKey(walletName),
);
res.status(200).send(dotAnything);
} catch (error) {
const walletInfo = await walletNameToAddressAndProfilePicture(CONNECTION, walletName);
res.status(200).send(walletInfo);
}
}
export default makeApiPostRequest(handler);
================================================
FILE: chatgpt-plugin/src/pages/api/helloMoon/defi/defi.yaml
================================================
openapi: 3.1.0
info:
title: HelloMoon Defi Summary API
description: An API for querying information about defi protocols on Solana
version: 1.0.0
servers:
- url: https://chatgpt.solanalabs.com/api/helloMoon
paths:
/defi/programNewUsers:
post:
summary: Shows new users of a program over time.
description:
Shows new users of a program over time. A new user is defined as the first time a publicKey
has been a fee payer for this program address.
operationId: query_program_new_users
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/programNewUsersRequest"
responses:
"200":
description: Successful Response
/defi/programOverlap:
post:
summary: Shows overlap between users of two different programs
description:
Program overlap shows the overlap between the users of two protocols. The data will show what percentage of 'aProgramIds' users also use 'bProgramId'
operationId: query_program_user_overlap
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/programOverlapRequest"
responses:
"200":
description: Successful Response
/defi/tokenUsers:
post:
summary:
description:
operationId: query_token_users
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/tokenUsersRequest"
responses:
"200":
description: Successful Response
/defi/tokenStats:
post:
summary:
description:
operationId: query_token_stats
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/tokenStatsRequest"
responses:
"200":
description: Successful Response
components:
schemas:
stringComparisonOperator:
type: string
enum:
- "="
- "!="
- ">"
- "<"
- ">="
nullable: true
nullableNumber:
type: number
nullable: true
programNewUsersRequest:
type: object
required:
- programId
properties:
programId:
type: string
day:
type: string
nullable: true
limit:
$ref: "#/components/schemas/nullableNumber"
programOverlapRequest:
type: object
properties:
aProgramId:
type: string
bProgramId:
type: string
limit:
$ref: "#/components/schemas/nullableNumber"
tokenUsersRequest:
type: object
properties:
day:
type: string
nullable: true
mint:
type: string
limit:
$ref: "#/components/schemas/nullableNumber"
tokenStatsRequest:
type: object
properties:
granularity:
type: string
enum:
- "one_week"
- "one_month"
- "one_day"
- "six_hour"
- "one_hour"
- "thirty_min"
nullable: true
mint:
type: string
nullable: true
limit:
$ref: "#/components/schemas/nullableNumber"
================================================
FILE: chatgpt-plugin/src/pages/api/helloMoon/defi/programNewUsers.ts
================================================
// Deprecated - Not in use
import { NextApiRequest, NextApiResponse } from "next";
import configConstants, { HELLOMOON_CLIENT } from "../../constants";
configConstants();
import { DefiProgramNetNewUsersDailyRequest } from "@hellomoon/api";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { programId, limit, day } = req.body;
let date: Date = new Date();
if (day) {
try {
date = new Date(day);
} catch (e) {
res.status(500).send("Invalid day: " + day);
return;
}
}
let args = new DefiProgramNetNewUsersDailyRequest({
programId,
limit: limit ?? 10,
day: date ? date.toISOString() : undefined,
});
let data = await HELLOMOON_CLIENT.send(args)
.then(result => result.data)
.catch(console.error);
res.status(200).send(data);
}
================================================
FILE: chatgpt-plugin/src/pages/api/helloMoon/defi/programOverlap.ts
================================================
// Deprecated - Not in use
import { NextApiRequest, NextApiResponse } from "next";
import configConstants, { HELLOMOON_CLIENT } from "../../constants";
import { DefiProgramOverlapRequest } from "@hellomoon/api";
configConstants();
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { aProgramId, bProgramId, limit } = req.body;
let args = new DefiProgramOverlapRequest({
aProgramId,
bProgramId,
limit: limit ?? 10,
});
let data = await HELLOMOON_CLIENT.send(args)
.then(result => result.data)
.catch(console.error);
res.status(200).send(data);
}
================================================
FILE: chatgpt-plugin/src/pages/api/helloMoon/defi/tokenStats.ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import configConstants, { HELLOMOON_CLIENT } from "../../constants";
configConstants();
import { DefiTokenLeaderboardV3Request } from "@hellomoon/api";
import { makeApiPostRequest } from "@/lib/middleware";
const GRANULARITY = ["one_day", "one_week", "one_month", "thirty_min", "one_hour", "six_hour"];
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { granularity, limit } = req.body;
if (granularity && !GRANULARITY.includes(granularity.toLowerCase())) {
res.status(500).send("Invalid granularity: " + granularity);
return;
}
let args = new DefiTokenLeaderboardV3Request({
granularity: granularity ? ((granularity as string).toLocaleUpperCase() as any) : "ONE_DAY",
mint: req.body["mint"].toString(),
limit: limit ?? 10,
});
let data = await HELLOMOON_CLIENT.send(args)
.then(result => result.data)
.catch(console.error);
res.status(200).send(data);
}
export default makeApiPostRequest(handler, { tokens: ["mint"] });
================================================
FILE: chatgpt-plugin/src/pages/api/helloMoon/defi/tokenUsers.ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import configConstants, { HELLOMOON_CLIENT } from "../../constants";
configConstants();
import { DefiTokenUsersDailyRequest } from "@hellomoon/api";
import { makeApiPostRequest } from "@/lib/middleware";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { day, limit } = req.body;
let args = new DefiTokenUsersDailyRequest({
mint: req.body["mint"].toString(),
day: day ? new Date(day).toISOString() : undefined,
limit: limit ?? 10,
});
let data = await HELLOMOON_CLIENT.send(args)
.then(result => result.data)
.catch(console.error);
res.status(200).send(data);
}
export default makeApiPostRequest(handler, { tokens: ["mint"] });
================================================
FILE: chatgpt-plugin/src/pages/api/helloMoon/jupiter/README.md
================================================
# Test Queries
#### Summary
Can you summarize Jupiter activity this past week on Solana?
```
In the past week, Jupiter on Solana has seen a total trading volume of approximately $11,179,154.79 USD. There have been 29,968 unique users and 686,025 transactions during this period. Overall, Jupiter has been quite active on Solana. Is there anything else you would like to know?
```
#### Historical
Can you just print out the last 7 days of jupiter volume in USD?
```
Here are the last 7 days of Jupiter volume in USD:
July 20, 2023: $7,557,612.35 USD
July 19, 2023: $10,989,070.90 USD
July 18, 2023: $12,249,619.75 USD
July 17, 2023: $16,730,144.59 USD
July 16, 2023: $10,583,573.39 USD
July 15, 2023: $17,052,417.78 USD
July 14, 2023: $36,812,293.04 USD
Please note that these values represent the daily trading volume on each respective day. Is there anything else you would like to know?
```
#### Pairs
What was mSOL - wSOL pair volume this week on Jupiter?
```
The mSOL - wSOL pair volume this week on Jupiter was approximately $229,340.42.
```
#### Swaps
What was the whole market volume of wSOL to mSOL swaps done on Solana this week?
```
The total market volume of wSOL to mSOL swaps done on Solana this week was $16,499,552.12.
```
What about the volume of mSOL - wSOL swaps just on Jupiter this week?
```
The volume of mSOL - wSOL swaps on Jupiter this week was $2,801,324.60.
```
================================================
FILE: chatgpt-plugin/src/pages/api/helloMoon/jupiter/historical.ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import configConstants, { HELLOMOON_CLIENT } from "../../constants";
configConstants();
import { Comparison, buildComparison } from "@/lib/helloMoon";
import { JupiterHistoricalTradingStatsRequest } from "@hellomoon/api";
import { makeApiPostRequest } from "@/lib/middleware";
const VALID_GRANULARITY = ["DAILY", "WEEKLY", "MONTHLY"];
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { granularity, limit } = req.body;
if (granularity && !VALID_GRANULARITY.includes((granularity as string).toLocaleUpperCase())) {
res
.status(500)
.send(
"Invalid granularity: " +
granularity +
". Valid granularities are: " +
VALID_GRANULARITY.join(", ") +
".",
);
return;
}
let numTxns: Comparison;
let usdVolume: Comparison;
let numUsers: Comparison;
try {
numTxns = buildComparison(req.body.numTxnsOperator, req.body.numTxnsValue);
usdVolume = buildComparison(req.body.usdVolumeOperator, req.body.usdVolumeValue);
numUsers = buildComparison(req.body.numUsersOperator, req.body.numUsersValue);
} catch (error) {
res.status(500).send((error as Error).message);
return;
}
let args = new JupiterHistoricalTradingStatsRequest({
granularity: ((granularity as string).toLocaleUpperCase() as any) ?? "DAILY",
limit: limit ?? 10,
numTxns,
usdVolume,
numUsers,
});
let data = await HELLOMOON_CLIENT.send(args)
.then(result => result.data)
.catch(console.error);
res.status(200).send(data);
}
export default makeApiPostRequest(handler);
================================================
FILE: chatgpt-plugin/src/pages/api/helloMoon/jupiter/pairs.ts
================================================
// DEPRECATED - not in use
import { NextApiRequest, NextApiResponse } from "next";
import configConstants, { HELLOMOON_CLIENT } from "../../constants";
import { Comparison, buildComparison, cleanSwapPair } from "@/lib/helloMoon";
import { JupiterSwapStatsRequest } from "@hellomoon/api";
configConstants();
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { swapPair, limit } = req.body;
let dailyVolume: Comparison;
let weeklyVolume: Comparison;
let monthlyVolume: Comparison;
try {
dailyVolume = buildComparison(req.body.usdVolume24HrOperator, req.body.usdVolume24HrValue);
weeklyVolume = buildComparison(req.body.usdVolume7DOperator, req.body.usdVolume7DValue);
monthlyVolume = buildComparison(req.body.usdVolume30DOperator, req.body.usdVolume30DValue);
} catch (error) {
res.status(500).send((error as Error).message);
return;
}
let args = new JupiterSwapStatsRequest({
swapPair: cleanSwapPair(swapPair),
limit: limit ?? 10,
usdVolume24Hr: dailyVolume,
usdVolume7D: weeklyVolume,
usdVolume30D: monthlyVolume,
});
let data = await HELLOMOON_CLIENT.send(args)
.then(result => result.data)
.catch(console.error);
res.status(200).send(data);
}
================================================
FILE: chatgpt-plugin/src/pages/api/helloMoon/jupiter/summary.ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import configConstants, { HELLOMOON_CLIENT } from "../../constants";
import { JupiterCurrentStatsRequest } from "@hellomoon/api";
import { makeApiPostRequest } from "@/lib/middleware";
configConstants();
async function handler(req: NextApiRequest, res: NextApiResponse) {
let data = await HELLOMOON_CLIENT.send(new JupiterCurrentStatsRequest({}))
.then(result => result.data[0])
.catch(console.error);
res.status(200).send(data);
}
export default makeApiPostRequest(handler);
================================================
FILE: chatgpt-plugin/src/pages/api/helloMoon/jupiter/swaps.ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import configConstants, { HELLOMOON_CLIENT } from "../../constants";
configConstants();
import { JupiterPairsBrokenDownWeeklyRequest } from "@hellomoon/api";
import { cleanSwapPair } from "@/lib/helloMoon";
import { makeApiPostRequest } from "@/lib/middleware";
const VALID_CATEGORY = ["per amm", "whole market", "jupiter only"];
// Make params case-insensitive so it's easier for LLMs to get right
const CATEGORY_MAP: { [key: string]: string } = {
"per amm": "Per AMM",
"whole market": "Whole market",
"jupiter only": "Jupiter only",
};
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { category, swapPair, limit } = req.body;
if (category && !VALID_CATEGORY.includes((category as string).toLocaleLowerCase())) {
res
.status(500)
.send(
"Invalid category: " +
category +
". Valid categories are: " +
VALID_CATEGORY.join(", ") +
".",
);
return;
}
let mapped = CATEGORY_MAP[category as string] as any;
let args = new JupiterPairsBrokenDownWeeklyRequest({
category: mapped,
swapPair: swapPair ? cleanSwapPair(swapPair) : undefined,
limit,
});
let data = await HELLOMOON_CLIENT.send(args)
.then(result => result.data)
.catch(console.error);
res.status(200).send(data);
}
export default makeApiPostRequest(handler);
================================================
FILE: chatgpt-plugin/src/pages/api/helloMoon/jupiter.yaml
================================================
openapi: 3.1.0
info:
title: HelloMoon API
description: An API for querying information about protocols built on Solana
version: 1.0.0
servers:
- url: https://chatgpt.solanalabs.com/api/helloMoon
paths:
/jupiter/summary:
post:
summary: Shows high-level information related to DeFi protocol Jupiter on Solana
description:
Shows the volume, number of users, and number of transactions on Jupiter over the past 24
hours, 7 days, and 30 days in USD
operationId: query_jupiter_summary
responses:
"200":
description: Successful Response
/jupiter/swaps:
post:
summary: Shows swap data on Jupiter protocol by amm
description:
Breakdown of swapping pair volume weekly on and off of Jupiter weekly. Data goes back 90
days. Data can be broken down by AMM if needed. Note that swapPair is the combination of the
source and destination symbol. The order will be in alphabetical order. For example, "USDC -
wSOL"
operationId: query_jupiter_swap_summary
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/jupiterSwapRequest"
responses:
"200":
description: Successful Response
/jupiter/historical:
post:
summary: Shows the volume, number of users, and number of transactions on Jupiter over time
description:
Shows the volume, number of users, and number of transactions on Jupiter over time
operationId: query_jupiter_historical_summary
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/jupiterHistoricalRequest"
responses:
"200":
description: Successful Response
/jupiter/pairs:
post:
summary: Shows the volume of swaps for token pairs on Jupiter
description:
Shows the volume of swaps for token pairs on Jupiter. Note that swapPair is the combination
of the source and destination symbol. The order will be in alphabetical order. For example,
"USDC - wSOL". Note that "USD" is not a valid token, and should be converted to "USDC" when
using this API. Also, 24hr is a day, 7D is a week, 30D is a month.
operationId: query_jupiter_pair_summary
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/jupiterPairsRequest"
responses:
"200":
description: Successful Response
components:
schemas:
stringComparisonOperator:
type: string
enum:
- "="
- "!="
- ">"
- "<"
- ">="
nullable: true
nullableNumber:
type: number
nullable: true
jupiterSwapRequest:
type: object
properties:
category:
type: string
enum:
- "whole market"
- "jupiter only"
- "per amm"
nullable: true
ammProgramId:
type: string
nullable: true
swapPair:
type: string
nullable: true
limit:
type: number
nullable: true
jupiterHistoricalRequest:
type: object
properties:
granularity:
type: string
enum:
- "daily"
- "weekly"
- "monthly"
nullable: true
limit:
$ref: "#/components/schemas/nullableNumber"
numTxnsOperator:
$ref: "#/components/schemas/stringComparisonOperator"
numTxnsValue:
$ref: "#/components/schemas/nullableNumber"
usdVolumeValue:
$ref: "#/components/schemas/nullableNumber"
usdVolumeOperator:
$ref: "#/components/schemas/stringComparisonOperator"
numUsersValue:
$ref: "#/components/schemas/nullableNumber"
numUsersOperator:
$ref: "#/components/schemas/stringComparisonOperator"
jupiterPairsRequest:
type: object
properties:
swapPair:
type: string
nullable: true
limit:
$ref: "#/components/schemas/nullableNumber"
usdVolume7DOperator:
$ref: "#/components/schemas/stringComparisonOperator"
usdVolume30DOperator:
$ref: "#/components/schemas/stringComparisonOperator"
usdVolume24HrOperator:
$ref: "#/components/schemas/stringComparisonOperator"
usdVolume7DValue:
$ref: "#/components/schemas/nullableNumber"
usdVolume30DValue:
$ref: "#/components/schemas/nullableNumber"
usdVolume24HrValue:
$ref: "#/components/schemas/nullableNumber"
================================================
FILE: chatgpt-plugin/src/pages/api/metaplex/getCNFTRent/index.ts
================================================
// Deprecated - Not in use
import {
ALL_DEPTH_SIZE_PAIRS,
DepthSizePair,
getConcurrentMerkleTreeAccountSize,
} from "@solana/spl-account-compression";
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
import { NextApiRequest, NextApiResponse } from "next";
import configConstants, { CONNECTION } from "../../constants";
configConstants();
function isValidDepthSizePair(maxDepth: number, maxBufferSize: number): boolean {
const pair: DepthSizePair = {
maxDepth,
maxBufferSize,
};
return ALL_DEPTH_SIZE_PAIRS.some(validPair => isEqualDepthSizePair(validPair, pair));
}
function isEqualDepthSizePair(pair1: DepthSizePair, pair2: DepthSizePair): boolean {
return pair1.maxDepth === pair2.maxDepth && pair1.maxBufferSize === pair2.maxBufferSize;
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method != "POST") {
res.status(405).send({ message: "Only POST requests allowed" });
return;
}
const { treeSize, canopySize, customMaxDepth, customMaxBufferSize, customCanopyDepth } = req.body;
let maxDepth: number, maxBufferSize: number, canopyDepth: number;
if (treeSize.toLowerCase() == "custom") {
if (customMaxDepth == undefined || customMaxBufferSize == undefined) {
res.status(400).send({ message: "Missing tree size custom parameters" });
return;
}
maxDepth = customMaxDepth;
maxBufferSize = customMaxBufferSize;
} else if (treeSize.toLowerCase() == "small") {
maxDepth = 14;
maxBufferSize = 64;
} else if (treeSize.toLowerCase() == "medium") {
maxDepth = 20;
maxBufferSize = 64;
} else if (treeSize.toLowerCase() == "large") {
maxDepth = 30;
maxBufferSize = 512;
} else {
res.status(400).send({ message: "Invalid tree size enum" });
return;
}
if (!isValidDepthSizePair(maxDepth, maxBufferSize)) {
res.status(400).send({ message: "Invalid tree Depth and Buffer Size pair" });
return;
}
if (canopySize.toLowerCase() == "custom") {
if (customCanopyDepth == undefined) {
res.status(400).send({ message: "Missing custom canopy depth parameter" });
return;
}
canopyDepth = customCanopyDepth;
} else if (canopySize.toLowerCase() == "small") {
canopyDepth = 3;
} else if (canopySize.toLowerCase() == "medium") {
canopyDepth = 14;
} else if (canopySize.toLowerCase() == "large") {
canopyDepth = 17;
} else if (canopySize.toLowerCase() == "none") {
canopyDepth = 0;
} else {
res.status(400).send({ message: "Invalid canopy size enum" });
return;
}
const requiredSpace = getConcurrentMerkleTreeAccountSize(maxDepth, maxBufferSize, canopyDepth);
const rent = await CONNECTION.getMinimumBalanceForRentExemption(requiredSpace);
res.status(200).send({
rentInSol: rent / LAMPORTS_PER_SOL,
maxNumberOfNFTs: 2 ** maxDepth,
});
}
================================================
FILE: chatgpt-plugin/src/pages/api/metaplex/getPublicTree/index.ts
================================================
// Deprecated - Not in use
import { NextApiRequest, NextApiResponse } from "next";
import configConstants from "../../constants";
configConstants();
const publicTreeAddresses: String[] = [
"3cnMCu5hezx9h6kjEYdTb7PRKch4a6KRFBoUF8SJEsT8",
"GNsnin9c2nDGp78E69tGXyMScWfysnu2PuxQxXy1jh3R",
];
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
res.status(200).send({ publicTreeAddresses: publicTreeAddresses });
}
================================================
FILE: chatgpt-plugin/src/pages/api/metaplex/metaplex.yaml
================================================
openapi: 3.0.2
info:
title: Metaplex API
description: An API for interacting with the Solana NFT collections using Metaplex
version: 1.0.0
servers:
- url: https://chatgpt.solanalabs.com
paths:
/api/handlers/solana-pay/qr/createMintCNFT:
post:
summary: createMintCNFT
description: >-
Mints a compressed NFT with the metadata uri. It does not need the rent or the treeAddress.
Returns a QR code that the user can scan to approve the transaction.
operationId: create_mint_cnft
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/createMintCNFTRequest"
responses:
"200":
description: Successful Response
/api/metaplex/getCNFTRent:
post:
summary: getCNFTRent
description: >-
Returns the rent for creating a tree on mainnet and the maximum capacity.
operationId: get_cnft_rent
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/getCNFTRentRequest"
responses:
"200":
description: Successful Response
/api/metaplex/getPublicTree:
post:
summary: getPublicTree
description: >-
Shows the public tree addresses with available space.
operationId: get_public_tree
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/getPublicTreeRequest"
responses:
"200":
description: Successful Response
/api/handlers/solana-pay/qr/createMintNFT:
post:
summary: createMintNFT
description: >-
Mints a normal uncompressed NFT with the given name, metadata uri and seller fees.
operationId: create_mint_nft
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/createMintNFTRequest"
responses:
"200":
description: Successful Response
/api/handlers/solana-pay/qr/createTransferAsset:
post:
summary: createTransferAsset
description: >-
Allows the user to send (transfer) compressed NFT to another account
operationId: create_transfer_asset
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/createTransferAssetRequest"
responses:
"200":
description: Successful Response
/api/handlers/solana-pay/qr/createBurnAsset:
post:
summary: createBurnAsset
description: >-
Allows the user to burn a compressed NFT
operationId: create_burn_asset
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/createBurnAssetRequest"
responses:
"200":
description: Successful Response
components:
schemas:
getCNFTRentRequest:
title: GetCNFTRentRequest
type: object
required:
- treeSize
- canopySize
properties:
treeSize:
type: string
enum:
- "custom"
- "small"
- "medium"
- "large"
canopySize:
type: string
enum:
- "custom"
- "small"
- "medium"
- "large"
- "none"
customMaxDepth:
type: number
nullable: true
customMaxBufferSize:
type: number
nullable: true
customCanopySize:
type: number
nullable: true
createMintCNFTRequest:
title: CreateMintCNFTRequest
type: object
required:
- metadataUri
properties:
metadataUri:
type: "string"
format: "uri"
maxLength: 255
createMintNFTRequest:
title: CreateMintNFTRequest
type: object
required:
- name
- metadataUri
properties:
name:
type: string
metadataUri:
type: string
format: "uri"
maxLength: 255
sellerFee:
type: number
createTransferAssetRequest:
type: object
required:
- destination
- assetId
properties:
destination:
type: string
assetId:
type: string
createBurnAssetRequest:
type: object
required:
- assetId
properties:
assetId:
type: string
getPublicTreeRequest:
type: object
properties: {}
================================================
FILE: chatgpt-plugin/src/pages/api/solflare-pfp/getProfilePic.ts
================================================
// Deprecated - Not in use
import { NextApiRequest, NextApiResponse } from "next";
import { getProfilePicture } from "@solflare-wallet/pfp";
import configConstants, { CONNECTION } from "../constants";
configConstants();
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method != "POST") {
res.status(405).send({ message: "Only POST requests allowed" });
return;
}
let publicKey = req.body.publicKey;
let pfpResult = await getProfilePicture(CONNECTION, publicKey);
res.status(200).send(pfpResult);
}
================================================
FILE: chatgpt-plugin/src/pages/api/tiplink/makeLink.ts
================================================
import { NextApiRequest, NextApiResponse } from "next";
import { TipLink } from "@tiplink/api";
import { makeApiPostRequest } from "@/lib/middleware";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const tp = await TipLink.create();
res.status(200).send({ url: tp.url, tipLinkAddress: tp.keypair.publicKey.toBase58() });
}
export default makeApiPostRequest(handler);
================================================
FILE: chatgpt-plugin/tailwind.config.js
================================================
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
},
},
},
plugins: [],
}
================================================
FILE: langchain_examples/README.md
================================================
# Langchain Usage
Langchain has support for using ChatGPTs in their Agent via their `tool` interface.
We have provided examples below
### Python Example
To run the python example, you will have to have an OpenAI key (you do not need GPT-4 access to run this example).
```bash
poetry install
cd langchain_examples
OPENAI_API_KEY=sk-xxxxxxx DEV=true python python/main.py
```
================================================
FILE: langchain_examples/python/main.py
================================================
from langchain.chat_models import ChatOpenAI
from langchain.agents import load_tools, initialize_agent
from langchain.agents import AgentType
from langchain.tools import AIPluginTool
if __name__ == '__main__':
# Setup environment variables
import os
DEV_ENV = os.environ['DEV'] == 'true'
URL = "http://localhost:3333" if DEV_ENV else "https://solana-gpt-plugin.onrender.com"
llm = ChatOpenAI(temperature=0)
# AI Agent does best when it only has one available tool
# to engage with URLs
tools = load_tools(["requests_post"])
# AIPluginTool only fetches and returns the openapi.yaml linked to in /.well-known/ai-plugin.json
# This may need some more work to avoid blowing up LLM context window
tool = AIPluginTool.from_plugin_url(URL + "/.well-known/ai-plugin.json")
tools += [tool]
# Setup an agent to answer the question without further human feedback
agent_chain = initialize_agent(
tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)
# Ask the question, and the agent loop
agent_chain.run(
"How many lamports does 8fbqVvpK3Dj7fdP2c8JJhtD7Zy3n9qtwAeGfbkgPu625 have?")
================================================
FILE: pyproject.toml
================================================
[tool.poetry]
name = "solana-gpt-plugin"
version = "0.1.0"
description = ""
authors = ["ngundotra "]
license = "Apache-2.0"
readme = "README.md"
packages = [{include = "solana_gpt_plugin"}]
[tool.poetry.dependencies]
python = "^3.9"
langchain = "^0.0.136"
requests = "^2.28.2"
openai = "^0.27.4"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
================================================
FILE: scripts/createNFTCollection.ts
================================================
import {
Metaplex,
toMetaplexFile,
UploadMetadataInput,
} from "@metaplex-foundation/js";
import {
createAssociatedTokenAccount,
createMint,
mintTo,
} from "@solana/spl-token";
import {
Keypair,
sendAndConfirmTransaction,
Transaction,
} from "@solana/web3.js";
import { savePublicKeyToFile } from "./helper";
import { PublicKey } from "@metaplex-foundation/js";
import {
createCreateMasterEditionV3Instruction,
createCreateMetadataAccountV3Instruction,
CreateMetadataAccountArgsV3,
createSetCollectionSizeInstruction,
PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID,
} from "@metaplex-foundation/mpl-token-metadata";
import * as fs from "fs";
import dotenv from "dotenv";
dotenv.config();
const ASSETS_DIR = "../chatgpt-plugin/public/assets/";
export async function createNFTCollection(payer: Keypair, metaplex: Metaplex) {
console.log(metaplex.connection);
// Create collection metadata
const buffer = fs.readFileSync(`${ASSETS_DIR}chatgpt-collection-logo.png`);
const file = toMetaplexFile(buffer, `chatgpt-collection-logo.png`);
console.log("before Uploading file to Metaplex");
const imageUri = await metaplex.storage().upload(file);
console.log("after Uploading file to Metaplex");
const data = fs.readFileSync(
`${ASSETS_DIR}chatgpt-collection-metadata.json`,
"utf-8"
);
const collectionInfo = JSON.parse(data);
const collectionMetadata: UploadMetadataInput = {
name: collectionInfo.name,
symbol: collectionInfo.symbol,
image: imageUri,
description: collectionInfo.description,
};
console.log("before Uploading metadata to Metaplex");
const { uri } = await metaplex.nfts().uploadMetadata(collectionMetadata);
console.log("after Uploading metadata to Metaplex");
// Create collection
const collectionMetadataV3: CreateMetadataAccountArgsV3 = {
data: {
name: `${collectionMetadata.name ?? ""}`,
symbol: `${collectionMetadata.symbol ?? ""}`,
uri,
sellerFeeBasisPoints: 100,
creators: [
{
address: payer.publicKey,
verified: true,
share: 100,
},
],
collection: null,
uses: null,
},
isMutable: false,
collectionDetails: null,
};
console.log("before creating mint");
const collectionMint = await createMint(
metaplex.connection,
payer,
payer.publicKey, // mintAuthority
payer.publicKey, // freezeAuthority
0 // collection -> decimal == 0
);
console.log("before creating account");
// const tokenAccount = await createAccount(
// metaplex.connection,
// payer,
// collectionMint,
// payer.publicKey,
// );
console.log(collectionMint.toString());
const tokenAccount = await createAssociatedTokenAccount(
metaplex.connection,
payer,
collectionMint,
payer.publicKey,
{ commitment: "confirmed" }
);
console.log("before minting");
await mintTo(
metaplex.connection,
payer,
collectionMint,
tokenAccount,
payer,
1,
[],
{ commitment: "confirmed" }
);
console.log("after minting");
const [collectionMetadataAccount, _bump] = PublicKey.findProgramAddressSync(
[
Buffer.from("metadata", "utf8"),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
collectionMint.toBuffer(),
],
TOKEN_METADATA_PROGRAM_ID
);
const createMetadataIx = createCreateMetadataAccountV3Instruction(
{
metadata: collectionMetadataAccount,
mint: collectionMint,
mintAuthority: payer.publicKey,
payer: payer.publicKey,
updateAuthority: payer.publicKey,
},
{
createMetadataAccountArgsV3: collectionMetadataV3,
}
);
// create account for showing supply of collection metadata, the proof of the Non-Fungible of the token
const [collectionMasterEditionAccount, _bump2] =
PublicKey.findProgramAddressSync(
[
Buffer.from("metadata", "utf8"),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
collectionMint.toBuffer(),
Buffer.from("edition", "utf8"),
],
TOKEN_METADATA_PROGRAM_ID
);
const createMasterEditionIx = createCreateMasterEditionV3Instruction(
{
edition: collectionMasterEditionAccount,
payer: payer.publicKey,
mint: collectionMint,
mintAuthority: payer.publicKey,
updateAuthority: payer.publicKey,
metadata: collectionMetadataAccount,
},
{
createMasterEditionArgs: {
maxSupply: 0,
},
}
);
// create collection size
const collectionSizeIx = createSetCollectionSizeInstruction(
{
collectionMetadata: collectionMetadataAccount,
collectionAuthority: payer.publicKey,
collectionMint: collectionMint,
},
{
setCollectionSizeArgs: { size: 1000000 },
}
);
try {
const tx = new Transaction();
tx.add(createMetadataIx);
tx.add(createMasterEditionIx);
tx.add(collectionSizeIx);
tx.feePayer = payer.publicKey;
// tx.sign(payer); ??
await sendAndConfirmTransaction(metaplex.connection, tx, [payer], {
commitment: "confirmed",
skipPreflight: true, //?
});
} catch (e) {
console.error("\nFailed to create collection:", e);
throw e;
}
savePublicKeyToFile(
"collectionMint",
collectionMint,
`${ASSETS_DIR}/chatgpt-collection-keys.json`
);
savePublicKeyToFile(
"collectionMetadataAccount",
collectionMetadataAccount,
`${ASSETS_DIR}chatgpt-collection-keys.json`
);
savePublicKeyToFile(
"collectionMasterEditionAccount",
collectionMasterEditionAccount,
`${ASSETS_DIR}chatgpt-collection-keys.json`
);
console.log("Collection Mint Address: ", collectionMint.toBase58());
console.log(
"Collection Metadata Address: ",
collectionMetadataAccount.toBase58()
);
console.log(
"Collection Master Edition Address: ",
collectionMasterEditionAccount.toBase58()
);
return {
collectionMint,
collectionMetadataAccount,
collectionMasterEditionAccount,
};
}
================================================
FILE: scripts/createTree.ts
================================================
import {
Keypair,
Connection,
PublicKey,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
ValidDepthSizePair,
createAllocTreeIx,
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
SPL_NOOP_PROGRAM_ID,
} from "@solana/spl-account-compression";
import {
PROGRAM_ID as BUBBLEGUM_PROGRAM_ID,
createCreateTreeInstruction,
} from "@metaplex-foundation/mpl-bubblegum";
import { savePublicKeyToFile } from "./helper";
import dotenv from "dotenv";
dotenv.config();
export interface MerkleTreeArgs {
maxDepth: number;
maxBufferSize: number;
canopyHeight: number;
}
export async function createTree(
payer: Keypair,
connection: Connection,
treeNumber: number | null,
canopyDepth: number,
maxDepthSizePair: ValidDepthSizePair
) {
const treeKeypair = Keypair.generate();
const [treeAuthority, _bump] = PublicKey.findProgramAddressSync(
[treeKeypair.publicKey.toBuffer()],
BUBBLEGUM_PROGRAM_ID
);
// allocate space for tree account instruction
const allocTreeIx = await createAllocTreeIx(
connection,
treeKeypair.publicKey,
payer.publicKey,
maxDepthSizePair,
canopyDepth
);
// create tree
const createTreeIx = createCreateTreeInstruction(
{
payer: payer.publicKey,
treeCreator: payer.publicKey,
treeAuthority: treeAuthority,
merkleTree: treeKeypair.publicKey,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
logWrapper: SPL_NOOP_PROGRAM_ID,
},
{
maxBufferSize: maxDepthSizePair.maxBufferSize,
maxDepth: maxDepthSizePair.maxDepth,
public: true,
},
BUBBLEGUM_PROGRAM_ID
);
try {
const tx = new Transaction();
tx.add(allocTreeIx);
tx.add(createTreeIx);
tx.feePayer = payer.publicKey;
await sendAndConfirmTransaction(connection, tx, [payer, treeKeypair], {
skipPreflight: true,
commitment: "confirmed",
});
} catch (e) {
console.error(e);
throw e;
}
savePublicKeyToFile(`treeAddress${treeNumber}`, treeKeypair.publicKey, "./assets/chatgpt-collection-keys.json");
savePublicKeyToFile(`treeAuthority${treeNumber}`, treeAuthority, "./assets/chatgpt-collection-keys.json");
console.log(`Tree Address${treeNumber}: `, treeKeypair.publicKey.toBase58());
return { treeAddress: treeKeypair.publicKey, treeAuthority };
}
================================================
FILE: scripts/helper.ts
================================================
import fs from "fs";
import path from "path";
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
} from "@solana/web3.js";
// define some default locations
const DEFAULT_KEY_DIR_NAME = ".local_keys";
const DEFAULT_PUBLIC_KEY_FILE = "keys.json";
const DEFAULT_NONCE_FILE = "nonce.json";
/*
Load locally stored PublicKey addresses
*/
export function loadPublicKeysFromFile(
absPath: string = `${DEFAULT_KEY_DIR_NAME}/${DEFAULT_PUBLIC_KEY_FILE}`
) {
try {
if (!absPath) throw Error("No path provided");
if (!fs.existsSync(absPath)) throw Error("File does not exist.");
// load the public keys from the file
const data =
JSON.parse(fs.readFileSync(absPath, { encoding: "utf-8" })) || {};
// convert all loaded keyed values into valid public keys
for (const [key, value] of Object.entries(data)) {
data[key] = new PublicKey(value as string) ?? "";
}
return data;
} catch (err) {
// console.warn("Unable to load local file");
}
// always return an object
return {};
}
/*
Locally save a PublicKey addresses to the filesystem for later retrieval
*/
export function savePublicKeyToFile(
name: string,
publicKey: PublicKey,
absPath: string = `${DEFAULT_KEY_DIR_NAME}/${DEFAULT_PUBLIC_KEY_FILE}`
) {
try {
// if (!absPath) throw Error("No path provided");
// if (!fs.existsSync(absPath)) throw Error("File does not exist.");
// fetch all the current values
let data: any = loadPublicKeysFromFile(absPath);
// convert all loaded keyed values from PublicKeys to strings
for (const [key, value] of Object.entries(data)) {
data[key as any] = (value as PublicKey).toBase58();
}
data = { ...data, [name]: publicKey.toBase58() };
// actually save the data to the file
fs.writeFileSync(absPath, JSON.stringify(data), {
encoding: "utf-8",
});
console.log("Saved to file: ", absPath);
// reload the keys for sanity
data = loadPublicKeysFromFile(absPath);
return data;
} catch (err) {
console.warn("Unable to save to file");
}
// always return an object
return {};
}
/*
Load locally stored nonce of nft counr
*/
export function loadNonceFromFile(
absPath: string = `${DEFAULT_KEY_DIR_NAME}/${DEFAULT_NONCE_FILE}`
) {
try {
if (!absPath) throw Error("No path provided");
if (!fs.existsSync(absPath)) throw Error("File does not exist.");
// load the public keys from the file
const data =
JSON.parse(fs.readFileSync(absPath, { encoding: "utf-8" })) || {};
// convert all loaded keyed values into valid public keys
for (const [key, value] of Object.entries(data)) {
data[key] = value as number;
}
return data;
} catch (err) {
// console.warn("Unable to load local file");
}
// always return an object
return {};
}
/*
Locally save a PublicKey addresses to the filesystem for later retrieval
*/
export function saveNonceToFile(
nonce: number,
absPath: string = `${DEFAULT_KEY_DIR_NAME}/${DEFAULT_NONCE_FILE}`
) {
try {
// if (!absPath) throw Error("No path provided");
// if (!fs.existsSync(absPath)) throw Error("File does not exist.");
// fetch all the current values
let data: any = loadNonceFromFile(absPath);
// convert all loaded keyed values from PublicKeys to strings
for (const [key, value] of Object.entries(data)) {
data[key as any] = value as number;
}
data = { nonce };
// actually save the data to the file
fs.writeFileSync(absPath, JSON.stringify(data), {
encoding: "utf-8",
});
// reload the keys for sanity
data = loadPublicKeysFromFile(absPath);
return data;
} catch (err) {
console.warn("Unable to save to file");
}
// always return an object
return {};
}
================================================
FILE: scripts/runCreateCollection.ts
================================================
import {
Metaplex,
bundlrStorage,
keypairIdentity,
} from "@metaplex-foundation/js";
import { Connection, Keypair } from "@solana/web3.js";
import dotenv from "dotenv";
import { uploadMetadata } from "./uploadMetadata";
dotenv.config();
(async () => {
console.log("process.env.HELIUS_API_KEY: ", process.env.HELIUS_API_KEY);
const HELIUS_URL = `https://rpc.helius.xyz/?api-key=${process.env.HELIUS_API_KEY}`;
// // const HELIUS_URL = `https://devnet.helius-rpc.com/?api-key=${process.env.HELIUS_API_KEY}`
const CONNECTION = new Connection(HELIUS_URL);
const payer = Keypair.fromSecretKey(
Uint8Array.from(
Buffer.from(process.env.COLLECTION_OWNER_SECRET_KEY as string, "base64")
)
);
console.log("collection owner(payer): ", payer.publicKey.toBase58());
const balance = await CONNECTION.getBalance(payer.publicKey);
console.log("balance: ", balance);
const metaplex = Metaplex.make(CONNECTION)
.use(keypairIdentity(payer))
.use(
bundlrStorage({
address: "https://node1.bundlr.network",
providerUrl: HELIUS_URL,
timeout: 60000,
})
);
//STEP 1: create a collection on mainnet/devnet
// await createNFTCollection(payer, metaplex);
//STEP 2: create a nft on mainnet/devnet
// following code is to create a sample metadata to be supplied to the chatbot, sample creating nft #1 for the collection
const uri = uploadMetadata(metaplex, 4);
// following is the code to create a tree on the devnet, on mainnet you have to use already created trees with empty leaves
// await createTree(payer, metaplex.connection, 1, 2, {
// maxDepth: 5,
// maxBufferSize: 8,
// });
})();
================================================
FILE: scripts/send.ts
================================================
// Deprecated - Not in use
import { Connection, Transaction, Keypair, Signer } from "@solana/web3.js";
import { readFileSync } from "fs";
const connection = new Connection("https://api.mainnet-beta.solana.com");
async function simulate(base64Transaction: string, signers: Signer[]) {
const transaction = Transaction.from(
Buffer.from(base64Transaction, "base64")
);
return await connection.simulateTransaction(transaction, signers);
}
function parse(): {
keypair: Keypair;
base64Transaction: string;
} {
let args = process.argv.slice(2);
if (args.length !== 4 || args[0] !== "--tx" || args[2] !== "--keypair") {
console.error(
"Usage: ts-node scripts/send.ts --tx --keypair "
);
process.exit(1);
}
const base64Transaction = args[1];
const keypairPath = args[3];
const keypair = Keypair.fromSecretKey(
new Uint8Array(JSON.parse(readFileSync(keypairPath, "utf-8")))
);
return { keypair, base64Transaction };
}
async function main() {
let { keypair, base64Transaction } = parse();
let simulationResult = await simulate(base64Transaction, [keypair]);
console.log(simulationResult);
}
main();
================================================
FILE: scripts/uploadMetadata.ts
================================================
// Deprecated - Not in use
import {
Metaplex,
toMetaplexFile,
UploadMetadataInput,
} from "@metaplex-foundation/js";
import * as fs from "fs";
const ASSETS_DIR = "../chatgpt-plugin/public/assets/";
export async function uploadMetadata(metaplex: Metaplex, nonce: number) {
const EXTENSION = "png";
const buffer = fs.readFileSync(`${ASSETS_DIR}${nonce}.${EXTENSION}`);
const file = toMetaplexFile(buffer, `${nonce}.${EXTENSION}`);
const imageUri = await metaplex.storage().upload(file);
console.log(`${ASSETS_DIR}${nonce}.json`);
const data = fs.readFileSync(`${ASSETS_DIR}${nonce}.json`, "utf-8");
const nftInfo = JSON.parse(data);
const nftMetadata: UploadMetadataInput = {
name: `${nftInfo.name ?? "NFT"}`,
symbol: `${nftInfo.symbol ?? "NFT"}`,
description: `${nftInfo.description ?? "NFT"}`,
image: imageUri,
properties: {
files: [
{
uri: `${nonce}.${EXTENSION}`,
type: `image/${EXTENSION}`,
},
],
},
};
const { uri } = await metaplex.nfts().uploadMetadata(nftMetadata);
console.log("metadata URI", uri);
}