npm package: @meteraprotocol/sdk
Description: SDK to interact with Metera's API & a UI component that will create orders into the Metera Protocol
Author: meteraprotocol
Latest version: 0.0.19

# SDK Usage Guide

## Overview

This SDK allows developers to interact with the **Metera API**, facilitating portfolio management, token operations, and order handling.

---

## Installation

Install the SDK via npm:

```bash
npm install @meteraprotocol/sdk
```

---

## Example Usage

### Fetching Portfolio Data

With the SDK installed, you can fetch portfolio data as follows:

```typescript
import { api } from '@meteraprotocol/sdk';

const config = new api.Configuration({
  basePath: 'https://metera-public-api-sample.com',
});

const sdk = api.SDKApiFactory();

const getPortfolios = async () => {
  return (await sdk.portfoliosGet()).data;
};

const getPortfolioState = async (portfolioId: string) => {
  return (await sdk.portfoliosStateIdGet(portfolioId)).data;
};

const getPortfolioPrice = async (
  portfolioId: string,
  period: api.PortfoliosPricePostRequestPeriodEnum,
) => {
  const sdk = api.SDKApiFactory();
  return (
    await sdk.portfoliosPricePost({
      portfolioId,
      period,
    })
  ).data;
};

getPortfolios()
  .then((res) => console.dir(res, { depth: null }))
  .catch(console.error);

getPortfolioState('porfolioId')
  .then((res) => console.dir(res, { depth: null }))
  .catch(console.error);

getPortfolioPrice('porfolioId', '7d')
  .then((res) => console.dir(res, { depth: null }))
  .catch(console.error);
```

### Fetching Token Prices and Weights

Token prices and weights can be retrieved and processed as follows:

```typescript
import { api } from '@meteraprotocol/sdk';
import { Prices } from '@meteraprotocol/core';
import { BigNumber } from 'bignumber.js';
import { BigRational } from 'big-rational-ts';

function bigNumberToBigRational(bigNumber: BigNumber): BigRational {
  if (bigNumber.eq(0)) return new BigRational(0n, 1n);
  const [numerator, denominator] = bigNumber
    .toFraction()
    .map((x) => BigInt(x.toFixed(0)));
  return new BigRational(numerator, denominator).reduce();
}

const portfolioState = (await sdk.portfoliosStateIdGet(portfolioId))
  .data as api.GetPortfolioStateResponse200;

const prices: Prices = Object.fromEntries(
  portfolioState.assets.map((asset) => {
    const rawPrice = bigNumberToBigRational(BigNumber(asset.price));
    const priceRootToken = rawPrice
      .div(new BigRational(10n ** BigInt(asset.asset.decimals), 1n))
      .reduce();
    return [asset.asset.id, priceRootToken];
  }),
);

const weights = Object.fromEntries(
  portfolioState.assets!.map((asset) => {
    const weight = new BigRational(
      BigInt(asset.weightNum),
      BigInt(asset.weightDenom),
    );
    return [asset.asset.id, weight];
  }),
);
```

#### Price Calculation

In blockchain systems, numerical values like token prices are often represented as integers (big integers) instead of decimals. This approach ensures precision and avoids rounding errors during on-chain computations. For instance:

- A price of `0.001` might be stored as `1` in the blockchain.
- To obtain a human-readable price, we divide the raw price by `10^decimals`, where `decimals` represents the token's precision.

The calculation involves:

1. Converting the raw price into a rational representation using `dbNumericToBigRational`.
2. Dividing the raw price by `10^decimals` to normalize it for human-readable usage.

This process retains the precision necessary for financial computations while aligning with blockchain-native practices of using integers.

---

## Placing an Order

All orders require the use of the `computeIntegration` function to handle transaction computations. The result of this function gives the necessary data to create a deposit or withdrawal order, including the token amounts and mtk supply of the state after the order is executed.

### Deposit Order

To create a deposit order:

```typescript
import { computeInteraction } from '@meteraprotocol/core';

const stateBeforeDeposit = {
  assets: Object.fromEntries(
    portfolioState.assets.map((a) => [a.asset.id, BigInt(a.amount)]),
  ),
  mtkSupply: BigInt(portfolioState.supply),
};

const stateAfterDeposit = computeInteraction(
  prices,
  weights,
  stateBeforeDeposit,
  new BigRational(50n, 1n), // Deposit 50 ADA
);

const tokensToDeposit = Object.entries(stateAfterDeposit.assets).map(
  ([id, amount]) => {
    const assetBefore = stateBeforeDeposit.assets[id];
    const assetAfter = amount.getNumerator() / amount.getDenominator();
    return { id, amount: (assetAfter - assetBefore).toString() };
  },
);

const depositOrder = (
  await api.ordersCreatePost({
    address: '<cardano_address>',
    portfolioId: '<portfolio_id>',
    tokens: tokensToDeposit,
    maxBatcherFee: portfolioState.batcherFee,
    minMtkAcceptable: '1',
  })
).data;
```

Knowing the state before and after the deposit, we can calculate the amount of tokens to be deposited and create the order. The `minMtkAcceptable` parameter is the minimum amount of MTKs that the user is willing to accept for the deposit (we recommend to set it at '1').

### Withdrawal Order

To create a withdrawal order we need to pass the minWorthAcceptable instead of minMtkAcceptable and also the amount of MTKs to be withdrawn.

```typescript
import { computeInteraction } from '@meteraprotocol/core';

const stateBeforeWithdraw = {
  assets: Object.fromEntries(
    portfolioState.assets.map((a) => [a.asset.id, BigInt(a.amount)]),
  ),
  mtkSupply: BigInt(portfolioState.supply),
};

const stateAfterWithdraw = computeInteraction(
  prices,
  weights,
  stateBeforeWithdraw,
  new BigRational(50n, 1n), // Withdraw 50 ADA
);

const tokensToWithdraw = Object.entries(stateAfterWithdraw.assets).map(
  ([id, amount]) => {
    const assetBefore = stateBeforeWithdraw.assets[id];
    const assetAfter = amount.getNumerator() / amount.getDenominator();
    return { id, amount: (assetAfter - assetBefore).toString() };
  },
);

const mtkAfter =
  stateAfterWithdraw.mtkSupply.getNumerator() /
  stateAfterWithdraw.mtkSupply.getDenominator();
const mtkBefore = stateBeforeWithdraw.mtkSupply;

const withdrawOrder = (
  await api.ordersCreatePost({
    address: '<cardano_address>',
    portfolioId: '<portfolio_id>',
    tokens: tokensToWithdraw,
    amount: (mtkBefore - mtkAfter).toString(),
    maxBatcherFee: portfolioState.batcherFee,
    minWorthAcceptable: '1',
  })
).data;
```

### Submitting the Order

After creating the order, you can submit it as follows, you will need a lucid instance with a wallet to sign the transaction.

```typescript
import { ErrorResponse } from '@meteraprotocol/sdk';

if ('error' in depositOrder) {
  console.log(depositOrder.error);
} else {
  const cbor = depositOrder.cbor;
  const signedTx = await lucid.fromTx(cbor).sign().complete();
  const signedCbor = signedTx.toString();
  console.log('Submitting order');
  const { data: res } = await sdk.ordersSubmitPost({
    cbor: signedCbor,
    id: depositOrder.id,
  });

  if (typeof res !== 'string') {
    console.log('Error submitting order', (res as api.ErrorResponse).error);
  } else {
    console.log('Order submitted', res);
  }
}
```

# UI Mint Burn Component Usage Guide

First of all you will need to instanciate the sdk and the lucid wallet. But now we import the ui related components and types from "@meteraprotocol/sdk/ui".

```typescript
import { api } from '@meteraprotocol/sdk';
import { MintBurn, WalletApi } from '@meteraprotocol/sdk/ui';
import type { NextPage } from 'next';
import { useEffect, useState } from 'react';
const Home = () => {
  const [wallet, setWallet] = useState<WalletApi | null>(null);
  const [portfolio, setPortfolio] = useState<IPortfolioState | null>(null);
  // load wallet & portfolio info
  useEffect(() => {
    const meteraAPIConfig = new api.Configuration({
      basePath: process.env.NEXT_PUBLIC_BACKEND_URL,
    });
    const meteraAPI = api.SDKApiFactory(meteraAPIConfig);

    const newWallet = await window.cardano[walletName].enable();
    setWallet(newWallet);
    loadPortfolio(meteraAPI, setPortfolio);
  }, []);
};
```

You will need to setup the following environment variable:

```env
NEXT_PUBLIC_BACKEND_URL=https://your-backend-url.com
```

Also this configuration on your next.config.js file is needed:

```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  webpack: (config) => {
    config.experiments = { ...config.experiments, topLevelAwait: true };
    return config;
  },
};

module.exports = nextConfig;
```

Now we need to fetch and parse the portfolio data. The following code takes a random portfolio and parses the response into a usable format:

```typescript
import { api } from '@meteraprotocol/sdk';
import { IPortfolioState } from '@meteraprotocol/sdk/ui/types';

export const loadPortfolio = async (
  meteraAPI: ReturnType<typeof SDKApiFactory>,
  setPortfolio: Dispatch<SetStateAction<IPortfolioState | null>>,
) => {
  try {
    const portfolios = await meteraAPI.portfoliosGet();
    if ('error' in portfolios.data) {
      console.log('ERROR FETCHING PORTFOLIOS');
      return;
    }
    const portfolioId = portfolios.data[0].id;
    const portfolioState = await meteraAPI.portfoliosStateIdGet(portfolioId);
    if ('error' in portfolioState.data) {
      console.log('ERROR FETCHING PORTFOLIO STATE');
      return;
    }
    setPortfolio(parseStatusResponse(portfolioState.data));
  } catch (err) {
    console.log(err);
  }
};

// THIS IS A HELPER FUNCTION THAT PARSES THE RESPONSE FROM THE API INTO A USABLE FORMAT
export const parseStatusResponse = (
  statusResponse: api.GetPortfolioStateResponse200,
) => ({
  portfolio: {
    ...statusResponse.portfolio,
    createdAt: new Date(statusResponse.portfolio.createdAt),
    featured: BigInt(statusResponse.portfolio.featured),
  },
  price: statusResponse.price,
  supply: BigInt(statusResponse.supply),
  platformFee: BigInt(statusResponse.platformFee),
  assets: statusResponse.assets.map((asset) => ({
    ...asset,
    priceCreatedAt: new Date(asset.priceCreatedAt),
    amount: BigInt(asset.amount),
    weightNum: BigInt(asset.weightNum),
    weightDenom: BigInt(asset.weightDenom),
  })),
  entryFee: BigInt(statusResponse.entryFee),
  exitFee: BigInt(statusResponse.exitFee),
  batcherFee: BigInt(statusResponse.batcherFee),
});
```

You can use the MintBurn component as follows:

```tsx
<MintBurn
  apiBaseUrl={process.env.NEXT_PUBLIC_BACKEND_URL!}
  network="Preview"
  portfolio={portfolio}
  type="mint"
  wallet={{ wallet }}
  //OPTIONAL STYLING
  containerProps={{
    width: '',
  }} // control the size of the modal. minWidth is 530px.
  background="" // string for the modal color
  primaryButtonColor="" // string for the main Buy button color
  hoverButtonColor="" // string for the hover Buy button color
/>
```

Styling the MintBurn component:

The minwidth

Now we are all set, you can now run your application and see the MintBurn component in action.
You should be able to see something like this:

![MintBurn frontend](./MintBurn.png)