Skip to content

Commit

Permalink
Merge branch 'main' into feat/earn
Browse files Browse the repository at this point in the history
  • Loading branch information
0xsambugs committed Jan 9, 2025
2 parents bef03e4 + 8863e38 commit b54d872
Show file tree
Hide file tree
Showing 20 changed files with 454 additions and 58 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
ALCHEMY_API_KEY: ${{secrets.ALCHEMY_API_KEY}}
BARTER_AUTH_HEADER: ${{secrets.BARTER_AUTH_HEADER}}
BARTER_CUSTOM_SUBDOMAIN: ${{secrets.BARTER_CUSTOM_SUBDOMAIN}}

CODEX_API_KEY: ${{secrets.CODEX_API_KEY}}
# integration-quotes:
# needs: ['integration']
# runs-on: ubuntu-latest
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ coverage
.env
newrelic_agent.log
yarn-error.log
cache
cache
.idea
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Balmy SDK

This repository contains the code for the Balmy sdk.
### [Docs](https://docs.balmy.xyz) | [X](https://x.com/balmy_xyz) | [Discord](http://discord.balmy.xyz/)

Balmy is the state-of-the-art DCA open protocol that enables users (or dapps) to Dollar Cost Average (DCA) any ERC20 into any ERC20 with their preferred period frequency, without sacrificing decentralization or giving up personal information to any centralized parties.

The Balmy SDK allows you to interact with the Balmy protocol, providing efficient tools to manage token balances, retrieve trade quotes from DEX aggregators, and check token holdings across multiple chains.

## 🧪 Installing

Expand All @@ -21,9 +25,9 @@ npm install @balmy/sdk
### 👷🏽‍♀️ Building the SDK

```javascript
import { buildSdk } from "@balmy/sdk";
import { buildSDK } from "@balmy/sdk";

const sdk = buildSdk(config);
const sdk = buildSDK(config);
```

### ⚖️ Getting balance for multiple tokens on several chains
Expand Down Expand Up @@ -108,7 +112,3 @@ await signer.sendTransaction(bestTradeBySort.tx);
```bash
yarn install
```

## 📖 Docs

WIP - Will be at [docs.balmy.xyz](https://docs.balmy.xyz)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@balmy/sdk",
"version": "0.4.8",
"version": "0.5.2",
"contributors": [
{
"name": "Nicolás Chamo",
Expand Down
17 changes: 11 additions & 6 deletions src/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,14 +322,19 @@ export const Chains = {
],
explorer: 'https://explorer.ont.io/',
},
KLAYTN: {
KAIA: {
chainId: 8217,
name: 'Klaytn',
ids: ['klaytn'],
nativeCurrency: { symbol: 'KLAY', name: 'Klaytn' },
name: 'Kaia',
ids: ['klaytn', 'kaia'],
nativeCurrency: { symbol: 'KAIA', name: 'Kaia' },
wToken: '0xe4f05a66ec68b54a58b17c22107b02e0232cc817',
publicRPCs: ['https://public-en-cypress.klaytn.net', 'https://public-node-api.klaytnapi.com/v1/cypress', 'https://klaytn-pokt.nodies.app'],
explorer: 'https://scope.klaytn.com/',
publicRPCs: [
'https://public-en-cypress.klaytn.net',
'https://public-node-api.klaytnapi.com/v1/cypress',
'https://klaytn-pokt.nodies.app',
'https://public-en.node.kaia.io',
],
explorer: 'https://kaiascan.io/',
},
AURORA: {
chainId: 1313161554,
Expand Down
9 changes: 8 additions & 1 deletion src/sdk/builders/price-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import { PrioritizedPriceSource } from '@services/prices/price-sources/prioritiz
import { FastestPriceSource } from '@services/prices/price-sources/fastest-price-source';
import { AggregatorPriceSource, PriceAggregationMethod } from '@services/prices/price-sources/aggregator-price-source';
import { BalmyPriceSource } from '@services/prices/price-sources/balmy-price-source';

import { CodexPriceSource } from '@services/prices/price-sources/codex-price-source';
import { AlchemyPriceSource } from '@services/prices/price-sources/alchemy-price-source';
export type PriceSourceInput =
| { type: 'defi-llama' }
| { type: 'codex'; apiKey: string }
| { type: 'odos' }
| { type: 'alchemy'; apiKey: string }
| { type: 'coingecko' }
| { type: 'balmy'; apiKey: string }
| { type: 'prioritized'; sources: PriceSourceInput[] }
Expand All @@ -35,6 +38,10 @@ function buildSource(source: PriceSourceInput | undefined, { fetchService }: { f
case undefined:
// Defi Llama is basically Coingecko with some token mappings. Defi Llama has a 5 min cache, so the priority will be Coingecko => DefiLlama
return new PrioritizedPriceSource([coingecko, defiLlama]);
case 'codex':
return new CodexPriceSource(fetchService, source.apiKey);
case 'alchemy':
return new AlchemyPriceSource(fetchService, source.apiKey);
case 'defi-llama':
return defiLlama;
case 'odos':
Expand Down
117 changes: 117 additions & 0 deletions src/services/prices/price-sources/alchemy-price-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { ChainId, TimeString, Timestamp, TokenAddress } from '@types';
import { IFetchService } from '@services/fetch/types';
import { PriceResult, IPriceSource, PricesQueriesSupport, TokenPrice, PriceInput } from '../types';
import { Chains, getChainByKeyOrFail } from '@chains';
import { reduceTimeout, timeoutPromise } from '@shared/timeouts';
import { filterRejectedResults, groupByChain, isSameAddress, splitInChunks } from '@shared/utils';
import { Addresses } from '@shared/constants';
import { ALCHEMY_NETWORKS } from '@shared/alchemy';
export class AlchemyPriceSource implements IPriceSource {
constructor(private readonly fetch: IFetchService, private readonly apiKey: string) {
if (!this.apiKey) throw new Error('API key is required');
}

supportedQueries() {
const support: PricesQueriesSupport = {
getCurrentPrices: true,
getHistoricalPrices: false,
getBulkHistoricalPrices: false,
getChart: false,
};
const entries = Object.entries(ALCHEMY_NETWORKS)
.filter(
([
_,
{
price: { supported },
},
]) => supported
)
.map(([chainId]) => [chainId, support]);
return Object.fromEntries(entries);
}

async getCurrentPrices({
tokens,
config,
}: {
tokens: PriceInput[];
config: { timeout?: TimeString } | undefined;
}): Promise<Record<ChainId, Record<TokenAddress, PriceResult>>> {
const groupedByChain = groupByChain(tokens, ({ token }) => token);
const reducedTimeout = reduceTimeout(config?.timeout, '100');
const promises = Object.entries(groupedByChain).map(async ([chainId, tokens]) => [
Number(chainId),
await timeoutPromise(this.getCurrentPricesInChain(Number(chainId), tokens, reducedTimeout), reducedTimeout),
]);
return Object.fromEntries(await filterRejectedResults(promises));
}

getHistoricalPrices(_: {
tokens: PriceInput[];
timestamp: Timestamp;
searchWidth: TimeString | undefined;
config: { timeout?: TimeString } | undefined;
}): Promise<Record<ChainId, Record<TokenAddress, PriceResult>>> {
// Only supports historical prices searching by token symbol
return Promise.reject(new Error('Operation not supported'));
}

getBulkHistoricalPrices(_: {
tokens: { chainId: ChainId; token: TokenAddress; timestamp: Timestamp }[];
searchWidth: TimeString | undefined;
config: { timeout?: TimeString } | undefined;
}): Promise<Record<ChainId, Record<TokenAddress, Record<Timestamp, PriceResult>>>> {
return Promise.reject(new Error('Operation not supported'));
}

async getChart(_: {
tokens: PriceInput[];
span: number;
period: TimeString;
bound: { from: Timestamp } | { upTo: Timestamp | 'now' };
searchWidth?: TimeString;
config: { timeout?: TimeString } | undefined;
}): Promise<Record<ChainId, Record<TokenAddress, PriceResult[]>>> {
return Promise.reject(new Error('Operation not supported'));
}

private async getCurrentPricesInChain(chainId: ChainId, addresses: TokenAddress[], timeout?: TimeString) {
const url = `https://api.g.alchemy.com/prices/v1/${this.apiKey}/tokens/by-address`;
const result: Record<TokenAddress, PriceResult> = {};
const chunks = splitInChunks(addresses, 25);
const promises = chunks.map(async (chunk) => {
const response = await this.fetch.fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
addresses: chunk.map((address) => ({
network: ALCHEMY_NETWORKS[chainId].key,
// Alchemy doesn't support native tokens (only on Ethereum), so we use the wrapped native token
address:
isSameAddress(address, Addresses.NATIVE_TOKEN) && chainId !== Chains.ETHEREUM.chainId
? getChainByKeyOrFail(chainId).wToken
: address,
})),
}),
timeout,
});

if (!response.ok) {
return;
}
const body: Response = await response.json();
chunk.forEach((address, index) => {
const tokenPrice = body.data[index].prices[0];
if (!tokenPrice) return;
const timestamp = Math.floor(new Date(tokenPrice.lastUpdatedAt).getTime() / 1000);
result[address] = { price: Number(tokenPrice.value), closestTimestamp: timestamp };
});
});

await Promise.all(promises);
return result;
}
}

type Response = { data: { address: TokenAddress; prices: { currency: string; value: string; lastUpdatedAt: string }[] }[] };
Loading

0 comments on commit b54d872

Please sign in to comment.