UNPKG

solana-options

Version:

Minting of options contract NFTs on the Solana blockchain

543 lines (482 loc) 20.1 kB
import { AccountLayout, MintLayout, Token, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from "@solana/spl-token"; import { Keypair, Signer } from "@solana/web3.js"; import { Connection, PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY, Transaction, TransactionInstruction } from "@solana/web3.js"; import BN from "bn.js"; import dayjs from "dayjs"; import { OPTION_ACCOUNT_DATA_LAYOUT } from "./layout"; import { print_contract, get_contract_from_blockchain, verify_contract } from "./utils"; export { get_contract_from_blockchain, verify_contract, print_contract }; import { publish_doc, create_doc_img } from "./doc"; export { publish_doc, create_doc_img }; import { TokenListProvider, TokenInfo } from '@solana/spl-token-registry'; import { SignerWalletAdapter } from "@solana/wallet-adapter-base"; const OPTIONS_PROGRAM_ID = process.env.OPTIONS_PROGRAM_ID || process.env.REACT_APP_OPTIONS_PROGRAM_ID || "DV4NugS55eXXposgxLnLr7WxySCTpaDd3cQPegFenHaj" const SEED = "optionsnft"; let TOKEN_LIST: TokenInfo[] = null const CLUSTER_SLUG = "mainnet-beta" export interface Contract { strike: number expiry: number multiple: number instrument: PublicKey strike_instrument: PublicKey nft_id?: PublicKey nft_account?: PublicKey account_id?: PublicKey collateral_acc?: PublicKey writer_recv_acc: PublicKey writer: PublicKey, kind: OptionType } export enum OptionType { call = 0, put = 1 } enum Instruction { create = 0, exercise = 1, create_new_nft_mint = 2, } async function get_token_map(): Promise<Map<string, string>> { if (TOKEN_LIST) { let symbol_to_address_map = new Map(TOKEN_LIST.map(t => [t.symbol, t.address])) return symbol_to_address_map } else { let tokens = await new TokenListProvider().resolve() const tokenList = tokens.filterByClusterSlug(CLUSTER_SLUG).getList(); TOKEN_LIST = tokenList; return get_token_map() } } /** * * @param connection * @param strike * @param expiry * @param multiple * @param creator_account * @param instrument * @param strike_instrument * @param creator_instrument_acc * @param creator_strike_instrument_acc * @returns */ export async function create_call(connection: Connection, strike: number, expiry: number, multiple: number, creator_account: Signer | SignerWalletAdapter, instrument: PublicKey | string | null, strike_instrument: PublicKey | string | null, creator_instrument_acc: PublicKey | null, creator_strike_instrument_acc: PublicKey | null): Promise<[string, Contract]> { console.log("creating call contract") return create_option(connection, strike, expiry, multiple, creator_account, instrument, strike_instrument, creator_instrument_acc, creator_strike_instrument_acc, OptionType.call) } /** * * @param connection * @param strike * @param expiry * @param multiple * @param creator_account * @param instrument * @param strike_instrument * @param creator_instrument_acc * @param creator_strike_instrument_acc * @returns */ export async function create_put(connection: Connection, strike: number, expiry: number, multiple: number, creator_account: Signer | SignerWalletAdapter, instrument: PublicKey | string | null, strike_instrument: PublicKey | string | null, creator_instrument_acc: PublicKey | null, creator_strike_instrument_acc: PublicKey | null): Promise<[string, Contract]> { console.log("creating put contract") return create_option(connection, strike, expiry, multiple, creator_account, instrument, strike_instrument, creator_instrument_acc, creator_strike_instrument_acc, OptionType.put) } export async function create_new_nft_mint(connection: Connection, multiple: number, creator_account: Signer | SignerWalletAdapter) { const instrument_mint_acc = new Keypair(); console.log("instrument mint account key: ", instrument_mint_acc.publicKey.toString()) const mint_rent = await connection.getMinimumBalanceForRentExemption(MintLayout.span, 'confirmed') console.log("using %s lamports to create the instrument mint account", mint_rent) const createInstrumentMintIx = SystemProgram.createAccount({ programId: TOKEN_PROGRAM_ID, space: MintLayout.span, lamports: await connection.getMinimumBalanceForRentExemption(MintLayout.span, 'confirmed'), fromPubkey: creator_account.publicKey, newAccountPubkey: instrument_mint_acc.publicKey }); // const instrument = instrument_mint_acc.publicKey // get the address for the account that will be associated with the NFT // this code is from the associated token program const [creator_instrument_acc, _] = await PublicKey.findProgramAddress( [ creator_account.publicKey.toBytes(), TOKEN_PROGRAM_ID.toBytes(), instrument_mint_acc.publicKey.toBytes() ], ASSOCIATED_TOKEN_PROGRAM_ID); console.log("instrument account key: ", creator_instrument_acc.toString()) const optionsProgramId = new PublicKey(OPTIONS_PROGRAM_ID); // call the program to initialize this mint. the program will be the mint authority for this mint const [pda, _bump_seed] = await PublicKey.findProgramAddress([Buffer.from(SEED)], optionsProgramId) let pda_account = new PublicKey(pda); const createNewNFTMintIx = new TransactionInstruction({ programId: optionsProgramId, keys: [{ pubkey: creator_account.publicKey, isSigner: true, isWritable: true }, { pubkey: creator_instrument_acc, isSigner: false, isWritable: true }, { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, { pubkey: pda_account, isSigner: false, isWritable: true }, { pubkey: instrument_mint_acc.publicKey, isSigner: false, isWritable: true } ], data: Buffer.from(Uint8Array.of(Instruction.create_new_nft_mint, ...new BN(multiple).toArray("le", 8))) }); console.log("sending the instructions ...") const tx = new Transaction() .add(createInstrumentMintIx, createNewNFTMintIx); if (!("secretKey" in creator_account)) { // this must be a wallet type then let wallet = creator_account as SignerWalletAdapter let sig = await wallet.sendTransaction(tx, connection, {signers: [instrument_mint_acc]}) return [sig, instrument_mint_acc, creator_instrument_acc] } let sig = await connection.sendTransaction(tx, [creator_account, instrument_mint_acc], { skipPreflight: false, preflightCommitment: 'finalized' }); return [sig, instrument_mint_acc, creator_instrument_acc] } export async function create_option(connection: Connection, strike: number, expiry: number, multiple: number, creator_account: Signer | SignerWalletAdapter, instrument: PublicKey | string | null, strike_instrument: PublicKey | string | null, creator_instrument_acc: PublicKey | null, creator_strike_instrument_acc: PublicKey, kind: OptionType): Promise<[string, Contract]> { // check if the either instrument or strike_instrument is a symbol or address(public); assume symbol if string if (typeof instrument == "string" || typeof strike_instrument == "string") { let symbol_to_address_map = await get_token_map() instrument = typeof instrument == "string" ? new PublicKey(symbol_to_address_map.get(instrument)) : instrument strike_instrument = typeof strike_instrument == "string" ? new PublicKey(symbol_to_address_map.get(strike_instrument)) : strike_instrument if (!strike_instrument) throw "invalid strike instrument symbol" } console.log("bob initiating contract") if (instrument == null) { // In this case, create a new NFT mint then assign it as the instrument if (creator_instrument_acc != null) throw "when instrument is null, the creator_instrument_acc must be null" //create a mint address for the new instrument console.log("creating new nft mint ..."); const [sig, instrument_mint_acc, new_creator_instrument_acc] = await create_new_nft_mint(connection, multiple, creator_account) creator_instrument_acc = new_creator_instrument_acc as PublicKey instrument = (instrument_mint_acc as Keypair).publicKey let res = await connection.confirmTransaction(sig as string, "finalized") console.log("new nft mint result", res) console.log("done creating new nft mint") } // create collateral account console.log("creating collateral acc") const collateralAccount: Signer = new Keypair(); console.log("collateral account key: ", collateralAccount.publicKey.toString()) const createCollateralAccIx = SystemProgram.createAccount({ programId: TOKEN_PROGRAM_ID, space: AccountLayout.span, lamports: await connection.getMinimumBalanceForRentExemption(AccountLayout.span, 'confirmed'), fromPubkey: creator_account.publicKey, newAccountPubkey: collateralAccount.publicKey }); // init collateral account let initCollateralAccountIx; if (kind == OptionType.call) { console.log("creating init call collateral acc instruction with instrument", instrument.toString()) initCollateralAccountIx = Token.createInitAccountInstruction(TOKEN_PROGRAM_ID, instrument, collateralAccount.publicKey, creator_account.publicKey); } else { console.log("creating init put collateral acc instruction") initCollateralAccountIx = Token.createInitAccountInstruction(TOKEN_PROGRAM_ID, strike_instrument, collateralAccount.publicKey, creator_account.publicKey); } // create options trading account (it is a program account not token account) console.log("creationg options program account create instruction") const optionsAccount: Signer = new Keypair(); const optionsProgramId = new PublicKey(OPTIONS_PROGRAM_ID); const createOptionsAccountIx = SystemProgram.createAccount({ space: OPTION_ACCOUNT_DATA_LAYOUT.span, lamports: await connection.getMinimumBalanceForRentExemption(OPTION_ACCOUNT_DATA_LAYOUT.span, 'singleGossip'), fromPubkey: creator_account.publicKey, newAccountPubkey: optionsAccount.publicKey, programId: optionsProgramId }); // Create a mint address that will hold the NFT attached to this contract let nftTokenAccount: Signer = new Keypair(); // get the address for the account that will be associated with the NFT // this code is from the associated token program const [nft_associated_account, _] = await PublicKey.findProgramAddress( [ creator_account.publicKey.toBytes(), TOKEN_PROGRAM_ID.toBytes(), nftTokenAccount.publicKey.toBytes() ], ASSOCIATED_TOKEN_PROGRAM_ID); console.log("nft token: ", nftTokenAccount.publicKey.toString()) console.log("nft associated account: ", nft_associated_account.toString()) const [pda, _bump_seed] = await PublicKey.findProgramAddress([Buffer.from(SEED)], optionsProgramId) let pda_account = new PublicKey(pda); const createOptionsIx = new TransactionInstruction({ programId: optionsProgramId, keys: [{ pubkey: creator_account.publicKey, isSigner: true, isWritable: true }, { pubkey: collateralAccount.publicKey, isSigner: false, isWritable: true }, { pubkey: creator_instrument_acc, isSigner: false, isWritable: true }, { pubkey: creator_strike_instrument_acc, isSigner: false, isWritable: true }, { pubkey: nftTokenAccount.publicKey, isSigner: true, isWritable: true }, { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, { pubkey: nft_associated_account, isSigner: false, isWritable: true }, { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, { pubkey: optionsAccount.publicKey, isSigner: false, isWritable: true }, { pubkey: pda_account, isSigner: false, isWritable: true } ], data: Buffer.from(Uint8Array.of(0, ...new BN(strike).toArray("le", 8), ...new BN(multiple).toArray("le", 8), ...new BN(expiry).toArray("le", 8), kind)) }); let contract: Contract = { strike: strike, expiry: expiry, multiple: multiple, instrument: instrument, strike_instrument: strike_instrument, nft_id: nftTokenAccount.publicKey, nft_account: nft_associated_account, account_id: optionsAccount.publicKey, collateral_acc: collateralAccount.publicKey, // a call means writer receives strike_instrument in exchange for instrument // a put means writer receives the instrument and send out the strike instrument writer_recv_acc: (kind == OptionType.call) ? creator_strike_instrument_acc : creator_instrument_acc, writer: creator_account.publicKey, kind: kind } // transfer tokens to temp and then get escrow info console.log("generated contract", print_contract(contract)) console.log("sending the instructions ...") const tx = new Transaction() .add(createCollateralAccIx, initCollateralAccountIx, createOptionsAccountIx, createOptionsIx); if (!("secretKey" in creator_account)) { // this must be a wallet type then let wallet = creator_account as SignerWalletAdapter let sig = await wallet.sendTransaction(tx, connection, {signers: [collateralAccount, optionsAccount, nftTokenAccount]}) console.log("done") return [sig, contract] } let sig = await connection.sendTransaction(tx, [creator_account, collateralAccount, optionsAccount, nftTokenAccount], { skipPreflight: false, preflightCommitment: 'finalized' }); console.log("done") return [sig, contract] } export async function exercise_call(connection: Connection, contract: Contract, buyer_acc: Signer | SignerWalletAdapter, buyer_nft_acc: PublicKey, buyer_receive_acc: PublicKey, buyer_send_acc: PublicKey) { return exercise_option(connection, contract, buyer_acc, buyer_nft_acc, buyer_receive_acc, buyer_send_acc, OptionType.call) } export async function exercise_put(connection: Connection, contract: Contract, buyer_acc: Signer | SignerWalletAdapter, buyer_nft_acc: PublicKey, buyer_receive_acc: PublicKey, buyer_send_acc: PublicKey) { return exercise_option(connection, contract, buyer_acc, buyer_nft_acc, buyer_receive_acc, buyer_send_acc, OptionType.put) } /** * Exercises the options contract * @param connection connection to the cluster * @param contract the Contract * @param buyer_acc buyer's account or buyers wallet * @param buyer_nft_acc the buyer's account that holds the ownership nft. This get burned by the exercise instruction * @param buyer_receive_acc account the buyers expects to receive the options collateral * @param buyer_send_acc the account holding the tokens the buyer is sending to exercise this contract * @param kind call or put * @returns signature */ export async function exercise_option(connection: Connection, contract: Contract, buyer_acc: Signer | SignerWalletAdapter, buyer_nft_acc: PublicKey, buyer_receive_acc: PublicKey, buyer_send_acc: PublicKey, kind: OptionType): Promise<string> { let strike = contract.strike; let expiry = contract.expiry; let multiple = contract.multiple; let today = dayjs(); if (today > dayjs(expiry * 1000)) { console.error("This contract exipred on %s , today is %s", dayjs(expiry).format(), today.format()) throw "contract has exipired" } const optionsProgramId = new PublicKey(OPTIONS_PROGRAM_ID); let nft_token_mint = new PublicKey(contract.nft_id) let collateral_acc = new PublicKey(contract.collateral_acc) let writer_recv_acc = new PublicKey(contract.writer_recv_acc) let options_program_account = new PublicKey(contract.account_id) const [pda, bump_seed] = await PublicKey.findProgramAddress([Buffer.from(SEED)], optionsProgramId) let pda_account = new PublicKey(pda); console.log("exercising contract", contract) const exerciseIx = new TransactionInstruction({ programId: optionsProgramId, keys: [{ pubkey: buyer_acc.publicKey, isSigner: true, isWritable: true }, { pubkey: buyer_nft_acc, isSigner: false, isWritable: true }, { pubkey: nft_token_mint, isSigner: false, isWritable: true }, { pubkey: buyer_send_acc, isSigner: false, isWritable: true }, { pubkey: buyer_receive_acc, isSigner: false, isWritable: true }, { pubkey: collateral_acc, isSigner: false, isWritable: true }, { pubkey: writer_recv_acc, isSigner: false, isWritable: true }, { pubkey: contract.writer, isSigner: false, isWritable: true }, { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, { pubkey: options_program_account, isSigner: false, isWritable: true }, { pubkey: pda_account, isSigner: false, isWritable: true }, { pubkey: SystemProgram.programId, isSigner: false, isWritable: false } ], data: Buffer.from(Uint8Array.of(1, ...new BN(strike).toArray("le", 8), ...new BN(multiple).toArray("le", 8), kind)) }); console.log("sending the exercise instructions ...") const tx = new Transaction() .add(exerciseIx); if (!("secretKey" in buyer_acc)) { // this must be a wallet type then let wallet = buyer_acc as SignerWalletAdapter return wallet.sendTransaction(tx, connection) } return connection.sendTransaction(tx, [buyer_acc], { skipPreflight: false, preflightCommitment: 'confirmed' }); } /** * Creators call this to close expired contracts. This instruction returns the collateral to the creator if the contract is expired and * hasn't been exercised yet, and returns any lamport used to create the options program account back to the creator and cleans out its data. * Exercised contracts are automatically closed * @param connection Connection to cluster * @param contract Contract * @param creator_acc The creators keypair * @param creator_receive_acc the receiving account where the released collateral will be sent back * @returns signature */ export async function close_option(connection: Connection, contract: Contract, creator_acc: Signer | SignerWalletAdapter, creator_receive_acc: PublicKey): Promise<string> { const optionsProgramId = new PublicKey(OPTIONS_PROGRAM_ID); let collateral_acc = new PublicKey(contract.collateral_acc) let options_program_account = new PublicKey(contract.account_id) const [pda, _] = await PublicKey.findProgramAddress([Buffer.from(SEED)], optionsProgramId) console.log("exercising contract", contract) const exerciseIx = new TransactionInstruction({ programId: optionsProgramId, keys: [{ pubkey: creator_acc.publicKey, isSigner: true, isWritable: true }, { pubkey: collateral_acc, isSigner: false, isWritable: true }, { pubkey: creator_receive_acc, isSigner: false, isWritable: true }, { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, { pubkey: options_program_account, isSigner: false, isWritable: true }, { pubkey: pda, isSigner: false, isWritable: true } ], data: Buffer.from(Uint8Array.of(3)) }); console.log("sending the close instructions ...") const tx = new Transaction() .add(exerciseIx); if (!("secretKey" in creator_acc)) { // this must be a wallet type then let wallet = creator_acc as SignerWalletAdapter return wallet.sendTransaction(tx, connection) } return connection.sendTransaction(tx, [creator_acc], { skipPreflight: false, preflightCommitment: 'confirmed' }); }