solana-options
Version:
Minting of options contract NFTs on the Solana blockchain
473 lines (472 loc) • 22.9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.close_option = exports.exercise_option = exports.exercise_put = exports.exercise_call = exports.create_option = exports.create_new_nft_mint = exports.create_put = exports.create_call = exports.OptionType = exports.create_doc_img = exports.publish_doc = exports.print_contract = exports.verify_contract = exports.get_contract_from_blockchain = void 0;
const spl_token_1 = require("@solana/spl-token");
const web3_js_1 = require("@solana/web3.js");
const web3_js_2 = require("@solana/web3.js");
const bn_js_1 = __importDefault(require("bn.js"));
const dayjs_1 = __importDefault(require("dayjs"));
const layout_1 = require("./layout");
const utils_1 = require("./utils");
Object.defineProperty(exports, "print_contract", { enumerable: true, get: function () { return utils_1.print_contract; } });
Object.defineProperty(exports, "get_contract_from_blockchain", { enumerable: true, get: function () { return utils_1.get_contract_from_blockchain; } });
Object.defineProperty(exports, "verify_contract", { enumerable: true, get: function () { return utils_1.verify_contract; } });
const doc_1 = require("./doc");
Object.defineProperty(exports, "publish_doc", { enumerable: true, get: function () { return doc_1.publish_doc; } });
Object.defineProperty(exports, "create_doc_img", { enumerable: true, get: function () { return doc_1.create_doc_img; } });
const spl_token_registry_1 = require("@solana/spl-token-registry");
const OPTIONS_PROGRAM_ID = process.env.OPTIONS_PROGRAM_ID || process.env.REACT_APP_OPTIONS_PROGRAM_ID || "DV4NugS55eXXposgxLnLr7WxySCTpaDd3cQPegFenHaj";
const SEED = "optionsnft";
let TOKEN_LIST = null;
const CLUSTER_SLUG = "mainnet-beta";
var OptionType;
(function (OptionType) {
OptionType[OptionType["call"] = 0] = "call";
OptionType[OptionType["put"] = 1] = "put";
})(OptionType = exports.OptionType || (exports.OptionType = {}));
var Instruction;
(function (Instruction) {
Instruction[Instruction["create"] = 0] = "create";
Instruction[Instruction["exercise"] = 1] = "exercise";
Instruction[Instruction["create_new_nft_mint"] = 2] = "create_new_nft_mint";
})(Instruction || (Instruction = {}));
async function get_token_map() {
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 spl_token_registry_1.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
*/
async function create_call(connection, strike, expiry, multiple, creator_account, instrument, strike_instrument, creator_instrument_acc, creator_strike_instrument_acc) {
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);
}
exports.create_call = create_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
*/
async function create_put(connection, strike, expiry, multiple, creator_account, instrument, strike_instrument, creator_instrument_acc, creator_strike_instrument_acc) {
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);
}
exports.create_put = create_put;
async function create_new_nft_mint(connection, multiple, creator_account) {
const instrument_mint_acc = new web3_js_1.Keypair();
console.log("instrument mint account key: ", instrument_mint_acc.publicKey.toString());
const mint_rent = await connection.getMinimumBalanceForRentExemption(spl_token_1.MintLayout.span, 'confirmed');
console.log("using %s lamports to create the instrument mint account", mint_rent);
const createInstrumentMintIx = web3_js_2.SystemProgram.createAccount({
programId: spl_token_1.TOKEN_PROGRAM_ID,
space: spl_token_1.MintLayout.span,
lamports: await connection.getMinimumBalanceForRentExemption(spl_token_1.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 web3_js_2.PublicKey.findProgramAddress([
creator_account.publicKey.toBytes(),
spl_token_1.TOKEN_PROGRAM_ID.toBytes(),
instrument_mint_acc.publicKey.toBytes()
], spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID);
console.log("instrument account key: ", creator_instrument_acc.toString());
const optionsProgramId = new web3_js_2.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 web3_js_2.PublicKey.findProgramAddress([Buffer.from(SEED)], optionsProgramId);
let pda_account = new web3_js_2.PublicKey(pda);
const createNewNFTMintIx = new web3_js_2.TransactionInstruction({
programId: optionsProgramId,
keys: [{
pubkey: creator_account.publicKey,
isSigner: true,
isWritable: true
}, {
pubkey: creator_instrument_acc,
isSigner: false,
isWritable: true
}, {
pubkey: spl_token_1.TOKEN_PROGRAM_ID,
isSigner: false,
isWritable: false
}, {
pubkey: web3_js_2.SYSVAR_RENT_PUBKEY,
isSigner: false,
isWritable: false
}, {
pubkey: web3_js_2.SystemProgram.programId,
isSigner: false,
isWritable: false
}, {
pubkey: spl_token_1.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_js_1.default(multiple).toArray("le", 8)))
});
console.log("sending the instructions ...");
const tx = new web3_js_2.Transaction()
.add(createInstrumentMintIx, createNewNFTMintIx);
if (!("secretKey" in creator_account)) {
// this must be a wallet type then
let wallet = creator_account;
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];
}
exports.create_new_nft_mint = create_new_nft_mint;
async function create_option(connection, strike, expiry, multiple, creator_account, instrument, strike_instrument, creator_instrument_acc, creator_strike_instrument_acc, kind) {
// 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 web3_js_2.PublicKey(symbol_to_address_map.get(instrument)) : instrument;
strike_instrument = typeof strike_instrument == "string" ? new web3_js_2.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;
instrument = instrument_mint_acc.publicKey;
let res = await connection.confirmTransaction(sig, "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 = new web3_js_1.Keypair();
console.log("collateral account key: ", collateralAccount.publicKey.toString());
const createCollateralAccIx = web3_js_2.SystemProgram.createAccount({
programId: spl_token_1.TOKEN_PROGRAM_ID,
space: spl_token_1.AccountLayout.span,
lamports: await connection.getMinimumBalanceForRentExemption(spl_token_1.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 = spl_token_1.Token.createInitAccountInstruction(spl_token_1.TOKEN_PROGRAM_ID, instrument, collateralAccount.publicKey, creator_account.publicKey);
}
else {
console.log("creating init put collateral acc instruction");
initCollateralAccountIx = spl_token_1.Token.createInitAccountInstruction(spl_token_1.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 = new web3_js_1.Keypair();
const optionsProgramId = new web3_js_2.PublicKey(OPTIONS_PROGRAM_ID);
const createOptionsAccountIx = web3_js_2.SystemProgram.createAccount({
space: layout_1.OPTION_ACCOUNT_DATA_LAYOUT.span,
lamports: await connection.getMinimumBalanceForRentExemption(layout_1.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 = new web3_js_1.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 web3_js_2.PublicKey.findProgramAddress([
creator_account.publicKey.toBytes(),
spl_token_1.TOKEN_PROGRAM_ID.toBytes(),
nftTokenAccount.publicKey.toBytes()
], spl_token_1.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 web3_js_2.PublicKey.findProgramAddress([Buffer.from(SEED)], optionsProgramId);
let pda_account = new web3_js_2.PublicKey(pda);
const createOptionsIx = new web3_js_2.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: spl_token_1.TOKEN_PROGRAM_ID,
isSigner: false,
isWritable: false
}, {
pubkey: web3_js_2.SYSVAR_RENT_PUBKEY,
isSigner: false,
isWritable: false
}, {
pubkey: web3_js_2.SystemProgram.programId,
isSigner: false,
isWritable: false
}, {
pubkey: nft_associated_account,
isSigner: false,
isWritable: true
}, {
pubkey: spl_token_1.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_js_1.default(strike).toArray("le", 8), ...new bn_js_1.default(multiple).toArray("le", 8), ...new bn_js_1.default(expiry).toArray("le", 8), kind))
});
let 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", (0, utils_1.print_contract)(contract));
console.log("sending the instructions ...");
const tx = new web3_js_2.Transaction()
.add(createCollateralAccIx, initCollateralAccountIx, createOptionsAccountIx, createOptionsIx);
if (!("secretKey" in creator_account)) {
// this must be a wallet type then
let wallet = creator_account;
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];
}
exports.create_option = create_option;
async function exercise_call(connection, contract, buyer_acc, buyer_nft_acc, buyer_receive_acc, buyer_send_acc) {
return exercise_option(connection, contract, buyer_acc, buyer_nft_acc, buyer_receive_acc, buyer_send_acc, OptionType.call);
}
exports.exercise_call = exercise_call;
async function exercise_put(connection, contract, buyer_acc, buyer_nft_acc, buyer_receive_acc, buyer_send_acc) {
return exercise_option(connection, contract, buyer_acc, buyer_nft_acc, buyer_receive_acc, buyer_send_acc, OptionType.put);
}
exports.exercise_put = exercise_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
*/
async function exercise_option(connection, contract, buyer_acc, buyer_nft_acc, buyer_receive_acc, buyer_send_acc, kind) {
let strike = contract.strike;
let expiry = contract.expiry;
let multiple = contract.multiple;
let today = (0, dayjs_1.default)();
if (today > (0, dayjs_1.default)(expiry * 1000)) {
console.error("This contract exipred on %s , today is %s", (0, dayjs_1.default)(expiry).format(), today.format());
throw "contract has exipired";
}
const optionsProgramId = new web3_js_2.PublicKey(OPTIONS_PROGRAM_ID);
let nft_token_mint = new web3_js_2.PublicKey(contract.nft_id);
let collateral_acc = new web3_js_2.PublicKey(contract.collateral_acc);
let writer_recv_acc = new web3_js_2.PublicKey(contract.writer_recv_acc);
let options_program_account = new web3_js_2.PublicKey(contract.account_id);
const [pda, bump_seed] = await web3_js_2.PublicKey.findProgramAddress([Buffer.from(SEED)], optionsProgramId);
let pda_account = new web3_js_2.PublicKey(pda);
console.log("exercising contract", contract);
const exerciseIx = new web3_js_2.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: spl_token_1.TOKEN_PROGRAM_ID,
isSigner: false,
isWritable: false
}, {
pubkey: options_program_account,
isSigner: false,
isWritable: true
}, {
pubkey: pda_account,
isSigner: false,
isWritable: true
}, {
pubkey: web3_js_2.SystemProgram.programId,
isSigner: false,
isWritable: false
}
],
data: Buffer.from(Uint8Array.of(1, ...new bn_js_1.default(strike).toArray("le", 8), ...new bn_js_1.default(multiple).toArray("le", 8), kind))
});
console.log("sending the exercise instructions ...");
const tx = new web3_js_2.Transaction()
.add(exerciseIx);
if (!("secretKey" in buyer_acc)) {
// this must be a wallet type then
let wallet = buyer_acc;
return wallet.sendTransaction(tx, connection);
}
return connection.sendTransaction(tx, [buyer_acc], { skipPreflight: false, preflightCommitment: 'confirmed' });
}
exports.exercise_option = exercise_option;
/**
* 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
*/
async function close_option(connection, contract, creator_acc, creator_receive_acc) {
const optionsProgramId = new web3_js_2.PublicKey(OPTIONS_PROGRAM_ID);
let collateral_acc = new web3_js_2.PublicKey(contract.collateral_acc);
let options_program_account = new web3_js_2.PublicKey(contract.account_id);
const [pda, _] = await web3_js_2.PublicKey.findProgramAddress([Buffer.from(SEED)], optionsProgramId);
console.log("exercising contract", contract);
const exerciseIx = new web3_js_2.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: spl_token_1.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 web3_js_2.Transaction()
.add(exerciseIx);
if (!("secretKey" in creator_acc)) {
// this must be a wallet type then
let wallet = creator_acc;
return wallet.sendTransaction(tx, connection);
}
return connection.sendTransaction(tx, [creator_acc], { skipPreflight: false, preflightCommitment: 'confirmed' });
}
exports.close_option = close_option;