UNPKG

@meteraprotocol/sdk

Version:

SDK to interact with Metera's API & a UI component that will create orders into the Metera Protocol

373 lines (297 loc) 11 kB
# 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)