resources_exchanges.js

import NinaClient from '../client';
import * as anchor from '@project-serum/anchor';
import { findOrCreateAssociatedTokenAccount, TOKEN_PROGRAM_ID } from '../utils';
/**
 * @module Exchange
 */

/**
 * @function fetchAll
 * @description Fetches all exchanges.
 * @param {Object} [pagination = {limit: 20, offset: 0, sort: 'desc'}] Pagination options.
 * @param {Boolean} [withAccountData = false] Include full on-chain Exchange accounts.
 * @example const exchanges = await NinaClient.Exchange.fetchAll();
 */
const fetchAll = async (pagination = {}, withAccountData = false) => {
  const { limit, offset, sort } = pagination;
  return await NinaClient.get(
    '/exchanges',
    {
      limit: limit || 20,
      offset: offset || 0,
      sort: sort || 'desc',
    },
    withAccountData
  );
};

/**
 * @function fetch
 * @description Fetches an exchange.
 * @param {String} publicKey The Public key of an Exchange account.
 * @param {Boolean} [withAccountData = false] Include full on-chain Exchange account.
 * @param {String} [transactionId = undefined] A transaction id from an interaction with an account -
 *        cancelling and completing Exchanges closes their on-chain accounts - by fetching a transaction
 *        we can have more context around interactions with an Exchange - useful for indexing.
 * @example const exchange = await NinaClient.Exchange.fetch('Fxv2G4cQQAeEXN2WkaSAusbNV7E1ouV9W6XHXf3DEfQ8');
 */
const fetch = async (publicKey, withAccountData = false, transactionId = undefined) => {
  return NinaClient.get(`/exchanges/${publicKey}`, transactionId ? { transactionId } : undefined, withAccountData);
};

/**
 * @function exchangeInit
 * @description Initializes an Exchange account.
 * @param {Object} client The NinaClient instance.
 * @param {String} exchangeAccount The public key of the Exchange account.
 * @param {Boolean} isSelling Whether the Exchange is selling or buying.
 * @param {String} releasePublicKey The public key of the Release account.
 * @returns {Object} The transaction id, the Exchange account, the Release Public Key, and the Exchange Signer.
 */

const exchangeInit = async (client, amount, isSelling, releasePublicKey) => {
  try {
    const { provider } = client;
    const program = await client.useProgram();

    let initializerSendingMint = null;
    let initializerExpectedMint = null;
    let expectedAmount = null;
    let initializerAmount = null;
    const release = new anchor.web3.PublicKey(releasePublicKey);
    const releaseAccount = await program.account.release.fetch(release);
    const releaseMint = releaseAccount.releaseMint;
    if (isSelling) {
      expectedAmount = new anchor.BN(amount);
      initializerSendingMint = releaseMint;
      initializerAmount = new anchor.BN(1);
      initializerExpectedMint = releaseAccount.paymentMint;
    } else {
      expectedAmount = new anchor.BN(1);
      initializerSendingMint = releaseAccount.paymentMint;
      initializerAmount = new anchor.BN(amount);
      initializerExpectedMint = releaseMint;
    }

    const exchange = anchor.web3.Keypair.generate();
    const [exchangeSigner, bump] = await anchor.web3.PublicKey.findProgramAddress(
      [exchange.publicKey.toBuffer()],
      program.programId
    );

    const [initializerSendingTokenAccount, initializerSendingTokenAccountIx] = await findOrCreateAssociatedTokenAccount(
      provider.connection,
      provider.wallet.publicKey,
      provider.wallet.publicKey,
      anchor.web3.SystemProgram.programId,
      anchor.web3.SYSVAR_RENT_PUBKEY,
      initializerSendingMint
    );

    const [exchangeEscrowTokenAccount, exchangeEscrowTokenAccountIx] = await findOrCreateAssociatedTokenAccount(
      provider.connection,
      provider.wallet.publicKey,
      exchangeSigner,
      anchor.web3.SystemProgram.programId,
      anchor.web3.SYSVAR_RENT_PUBKEY,
      initializerSendingMint
    );

    const [initializerExpectedTokenAccount, initializerExpectedTokenAccountIx] =
      await findOrCreateAssociatedTokenAccount(
        provider.connection,
        provider.wallet.publicKey,
        provider.wallet.publicKey,
        anchor.web3.SystemProgram.programId,
        anchor.web3.SYSVAR_RENT_PUBKEY,
        initializerExpectedMint
      );
    const exchangeCreateIx = await program.account.exchange.createInstruction(exchange);

    let accounts = {
      initializer: provider.wallet.publicKey,
      releaseMint,
      initializerExpectedTokenAccount,
      initializerSendingTokenAccount,
      initializerExpectedMint,
      initializerSendingMint,
      exchangeEscrowTokenAccount,
      exchangeSigner,
      exchange: exchange.publicKey,
      release,
      systemProgram: anchor.web3.SystemProgram.programId,
      tokenProgram: TOKEN_PROGRAM_ID,
      rent: anchor.web3.SYSVAR_RENT_PUBKEY,
    };
    let signers = [exchange];
    let instructions = [exchangeCreateIx, exchangeEscrowTokenAccountIx];

    if (initializerExpectedTokenAccountIx) {
      instructions.push(initializerExpectedTokenAccountIx);
    }

    if (initializerSendingTokenAccountIx) {
      instructions.push(initializerSendingTokenAccountIx);
    }

    if (client.isSol(releaseAccount.paymentMint) && !isSelling) {
      const [wrappedSolAccount, wrappedSolInstructions] = await wrapSol(
        provider,
        initializerAmount,
        new anchor.web3.PublicKey(client.ids.mints.wsol)
      );
      if (!instructions) {
        instructions = [...wrappedSolInstructions];
      } else {
        instructions.push(...wrappedSolInstructions);
      }
      accounts.initializerSendingTokenAccount = wrappedSolAccount;
    }
    const config = {
      expectedAmount,
      initializerAmount,
      isSelling,
    };
    const tx = await program.methods
      .exchangeInit(config, bump)
      .accounts(accounts)
      .preInstructions(instructions)
      .signers(signers)
      .transaction();

    tx.recentBlockhash = (await provider.connection.getRecentBlockhash()).blockhash;
    tx.feePayer = provider.wallet.publicKey;
    for await (let signer of signers) {
      tx.partialSign(signer);
    }
    const txid = await provider.wallet.sendTransaction(tx, provider.connection);
    await provider.connection.getParsedTransaction(txid, 'confirmed');
    const publicKey = exchange.publicKey;
    return {
      txid,
      releaseAccount,
      publicKey,
      releaseMint,
    };
  } catch (err) {
    console.error(err);
    return false;
  }
};

/**
 * @function exchangeAccept
 * @description Initializes an Exchange account.
 * @param {Object} client The NinaClient instance.
 * @param {String} exchangeAccount The public key of the Exchange account.
 * @param {String} releasePublicKey The public key of the Release account.
 */

const exchangeAccept = async (client, exchange, releasePublicKey) => {
  try {
    const { provider } = client;
    const program = await client.useProgram();
    const releaseKey = new anchor.web3.PublicKey(releasePublicKey);
    const release = await program.account.release.fetch(releaseKey);
    const exchangePubkey = new anchor.web3.PublicKey(exchange.publicKey);
    const exchangeAccount = await program.account.exchange.fetch(exchangePubkey);

    const [takerSendingTokenAccount, takerSendingTokenAccountIx] = await findOrCreateAssociatedTokenAccount(
      provider.connection,
      provider.wallet.publicKey,
      provider.wallet.publicKey,
      anchor.web3.SystemProgram.programId,
      anchor.web3.SYSVAR_RENT_PUBKEY,
      exchangeAccount.initializerExpectedMint
    );

    const [takerExpectedTokenAccount, takerExpectedTokenAccountIx] = await findOrCreateAssociatedTokenAccount(
      provider.connection,
      provider.wallet.publicKey,
      provider.wallet.publicKey,
      anchor.web3.SystemProgram.programId,
      anchor.web3.SYSVAR_RENT_PUBKEY,
      exchangeAccount.initializerSendingMint
    );

    const [initializerExpectedTokenAccount, initializerExpectedTokenAccountIx] =
      await findOrCreateAssociatedTokenAccount(
        provider.connection,
        provider.wallet.publicKey,
        exchangeAccount.initializer,
        anchor.web3.SystemProgram.programId,
        anchor.web3.SYSVAR_RENT_PUBKEY,
        exchangeAccount.initializerExpectedMint
      );

    const exchangeHistory = anchor.web3.Keypair.generate();
    const createExchangeHistoryIx = await program.account.exchangeHistory.createInstruction(exchangeHistory);
    let instructions = [createExchangeHistoryIx];
    let accounts = {
      initializer: exchangeAccount.initializer,
      initializerExpectedTokenAccount,
      takerExpectedTokenAccount,
      takerSendingTokenAccount,
      exchangeEscrowTokenAccount: exchangeAccount.exchangeEscrowTokenAccount,
      exchangeSigner: exchangeAccount.exchangeSigner,
      taker: provider.wallet.publicKey,
      exchange: new anchor.web3.PublicKey(exchange.publicKey),
      exchangeHistory: exchangeHistory.publicKey,
      release: new anchor.web3.PublicKey(releasePublicKey),
      royaltyTokenAccount: release.royaltyTokenAccount,
      tokenProgram: TOKEN_PROGRAM_ID,
      systemProgram: anchor.web3.SystemProgram.programId,
      rent: anchor.web3.SYSVAR_RENT_PUBKEY,
    };

    if (takerSendingTokenAccountIx) {
      instructions.push(takerSendingTokenAccountIx);
    }
    if (takerExpectedTokenAccountIx) {
      instructions.push(takerExpectedTokenAccountIx);
    }
    if (initializerExpectedTokenAccountIx) {
      instructions.push(initializerExpectedTokenAccountIx);
    }

    if (client.isSol(release.paymentMint) && exchange.isSelling) {
      const [wrappedSolAccount, wrappedSolInstructions] = await wrapSol(
        provider,
        exchange.expectedAmount,
        release.paymentMint
      );
      instructions.push(...wrappedSolInstructions);
      accounts.takerSendingTokenAccount = wrappedSolAccount;
    }
    const params = {
      expectedAmount: exchangeAccount.expectedAmount,
      initializerAmount: exchangeAccount.initializerAmount,
      resalePercentage: release.resalePercentage,
      datetime: new anchor.BN(Date.now() / 1000),
    };

    const tx = await program.methods
      .exchangeAccept(params)
      .accounts(accounts)
      .preInstructions(instructions)
      .signers([exchangeHistory])
      .transaction();
    tx.recentBlockhash = (await provider.connection.getRecentBlockhash()).blockhash;
    tx.feePayer = provider.wallet.publicKey;
    for await (let signer of [exchangeHistory]) {
      tx.partialSign(signer);
    }
    const signedTx = await provider.wallet.signTransaction(tx);
    const txid = await provider.connection.sendRawTransaction(signedTx.serialize(), {
      skipPreflight: true,
    });
    await provider.connection.getParsedTransaction(txid, 'finalized');
    return txid;
  } catch (err) {
    console.error(err);
    return false;
  }
};

/**
 * @function exchangeCancel
 * @description Cancels an initialized Exchange.
 * @param {Object} client The NinaClient instance.
 * @param {String} exchangeAccount The public key of the Exchange account.
 * @example const canceledExchange = await exchangeCancel(client, exchangeAccount);
 * @returns {Object} The transaction id and the public key of the Exchange.
 */

const exchangeCancel = async (client, exchange) => {
  const exchangePubkey = new anchor.web3.PublicKey(exchange.publicKey);
  const { provider } = client;
  const program = await client.useProgram();
  exchange = await program.account.exchange.fetch(exchangePubkey);
  const [initializerReturnTokenAccount, initializerReturnTokenAccountIx] = await findOrCreateAssociatedTokenAccount(
    provider.connection,
    provider.wallet.publicKey,
    provider.wallet.publicKey,
    anchor.web3.SystemProgram.programId,
    anchor.web3.SYSVAR_RENT_PUBKEY,
    exchange.initializerSendingMint
  );

  let instructions;
  if (initializerReturnTokenAccountIx) {
    instructions.push(initializerReturnTokenAccountIx);
  }

  let tx;
  const params = new anchor.BN(exchange.isSelling ? 1 : exchange.initializerAmount.toNumber());
  if (client.isSol(exchange.initializerSendingMint)) {
    tx = await program.methods
      .exchangeCancelSol(params)
      .accounts({
        initializer: provider.wallet.publicKey,
        initializerSendingTokenAccount: initializerReturnTokenAccount,
        exchangeEscrowTokenAccount: exchange.exchangeEscrowTokenAccount,
        exchangeSigner: exchange.exchangeSigner,
        exchange: exchangePubkey,
        tokenProgram: TOKEN_PROGRAM_ID,
      })
      .preInstructions(instructions || [])
      .signers([])
      .transaction();
  } else {
    tx = await program.methods
      .exchangeCancel(params)
      .accounts({
        initializer: provider.wallet.publicKey,
        initializerSendingTokenAccount: initializerReturnTokenAccount,
        exchangeEscrowTokenAccount: exchange.exchangeEscrowTokenAccount,
        exchangeSigner: exchange.exchangeSigner,
        exchange: exchangePubkey,
        tokenProgram: TOKEN_PROGRAM_ID,
      })
      .preInstructions(instructions || [])
      .signers([])
      .transaction();
  }

  tx.recentBlockhash = (await provider.connection.getRecentBlockhash()).blockhash;
  tx.feePayer = provider.wallet.publicKey;
  const txid = await provider.wallet.sendTransaction(tx, provider.connection);
  await provider.connection.getParsedTransaction(txid, 'confirmed');
  return {
    exchangePubkey,
    txid,
  };
};

export default {
  fetchAll,
  fetch,
  exchangeInit,
  exchangeAccept,
  exchangeCancel,
};