@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
Markdown
This SDK allows developers to interact with the **Metera API**, facilitating portfolio management, token operations, and order handling.
---
Install the SDK via npm:
```bash
npm install @meteraprotocol/sdk
```
---
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);
```
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];
}),
);
```
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;
```
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);
}
}
```
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:
