Skip to content

Commit

Permalink
feat: support Explorer Asset API (#3648)
Browse files Browse the repository at this point in the history
* chore: shuffled assets directory

* chore: changeset

* chore: removed assets.json

* chore: fix release path

* chore: added basic API with live tests

* chore: finalized API w/ mocks

* chore: changeset

* chore: alter the API

* chore: fix tests

* chore: finalizing functionality

* chore: added docs

* chore: added speeling

* chore: added explorer api endpoints to ignore

* chore: removed now deployed docs

* chore: fix tests

* docs: moved asset API

* docs: moved asset api snippets

* chore: refactored to use `network`

---------

Co-authored-by: Anderson Arboleya <[email protected]>
  • Loading branch information
petertonysmith94 and arboleya authored Feb 3, 2025
1 parent d35ccb7 commit 8030180
Show file tree
Hide file tree
Showing 11 changed files with 611 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/rich-items-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/account": patch
---

feat: support Explorer Asset API
4 changes: 4 additions & 0 deletions apps/docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,10 @@ export default defineConfig({
text: 'Using assets',
link: '/guide/utilities/using-assets',
},
{
text: 'Asset API',
link: '/guide/utilities/asset-api',
},
],
},
{
Expand Down
3 changes: 2 additions & 1 deletion apps/docs/spell-check-custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -343,4 +343,5 @@ Workspaces
WSL
XOR
XORs
YAML
YAML
RESTful
30 changes: 30 additions & 0 deletions apps/docs/src/guide/utilities/asset-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Asset API

The Asset API is a RESTful API that allows you to query the assets on the Fuel blockchain. We allow for querying the Asset API on both the Mainnet and Testnet.

| | Endpoint |
| ------- | --------------------------------------------- |
| Mainnet | https://mainnet-explorer.fuel.network |
| Testnet | https://explorer-indexer-testnet.fuel.network |

For more information about the API, please refer to the [Wiki](https://github.com/FuelLabs/fuel-explorer/wiki/Assets-API#) page.

## Asset by ID

We can request information about an asset by its asset ID, using the `getAssetById` function. This will leverage the endpoint `/assets/<assetId>` to fetch the asset information.

<<< @./snippets/asset-api/asset-by-id.ts#full{ts:line-numbers}

By default, we will request the asset information for `mainnet`. If you want to request the asset information from other networks, you can pass the `network` parameter (this is the same for the [`getAssetsByOwner`](#assets-by-owner) function).

<<< @./snippets/asset-api/asset-by-id.ts#testnet{ts:line-numbers}

## Assets by Owner

We can request information about an asset by its owner, using the `getAssetsByOwner` function. This will leverage the endpoint `/accounts/<owner>/assets` to fetch the asset information.

<<< @./snippets/asset-api/assets-by-owner.ts#full{ts:line-numbers}

You can change the pagination parameters to fetch more assets (up to 100 assets per request).

<<< @./snippets/asset-api/assets-by-owner.ts#pagination{ts:line-numbers}
18 changes: 18 additions & 0 deletions apps/docs/src/guide/utilities/snippets/asset-api/asset-by-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// #region full
import type { AssetInfo } from 'fuels';
import { getAssetById } from 'fuels';

const asset: AssetInfo | null = await getAssetById({
assetId: '0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07',
});

console.log('AssetInfo', asset);
// AssetInfo { ... }
// #endregion full

// #region testnet
await getAssetById({
assetId: '0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07',
network: 'testnet',
});
// #endregion testnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// #region full
import type { AssetsByOwner } from 'fuels';
import { getAssetsByOwner } from 'fuels';

const assets: AssetsByOwner = await getAssetsByOwner({
owner: '0x0000000000000000000000000000000000000000000000000000000000000000',
});

console.log('AssetsByOwner', assets);
// AssetsByOwner { data: [], pageInfo: { count: 0 } }
// #endregion full

// #region pagination
await getAssetsByOwner({
owner: '0x0000000000000000000000000000000000000000000000000000000000000000',
pagination: { last: 100 },
});
// #endregion pagination
6 changes: 6 additions & 0 deletions link-check.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
"ignorePatterns": [
{
"pattern": "^http://localhost:3000"
},
{
"pattern": "https://mainnet-explorer.fuel.network"
},
{
"pattern": "https://explorer-indexer-testnet.fuel.network"
}
]
}
194 changes: 194 additions & 0 deletions packages/account/src/assets/asset-api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { MOCK_ASSET_INFO_BY_OWNER, MOCK_BASE_ASSET, MOCK_FUEL_ASSET, MOCK_NFT_ASSET } from "../../test/fixtures/assets";
import { getAssetById, getAssetsByOwner, AssetInfo } from "./asset-api";

const mockFetch = () => {
const jsonResponse = vi.fn();
const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValueOnce({
json: jsonResponse
} as unknown as Response);

return {
fetch: fetchSpy,
json: jsonResponse
}
}

/**
* @group node
* @group browser
*/
describe('Asset API', () => {
describe('getAssetById', () => {
it('should get an asset by id [Base Asset - Verified]', async () => {
const expected = {
assetId: expect.any(String),
name: expect.any(String),
symbol: expect.any(String),
decimals: expect.any(Number),
rate: expect.any(Number),
icon: expect.any(String),
suspicious: expect.any(Boolean),
verified: expect.any(Boolean),
networks: expect.any(Array),
}
const { fetch, json } = mockFetch();
json.mockResolvedValueOnce(MOCK_BASE_ASSET);

const response = await getAssetById({
assetId: MOCK_BASE_ASSET.assetId
});

const assetInfo = response as AssetInfo;
expect(assetInfo).toEqual(MOCK_BASE_ASSET)
expect(assetInfo).toMatchObject(expected)
expect(Object.keys(assetInfo).sort()).toEqual(Object.keys(expected).sort())
})

it('should get an asset by id [Fuel - Verified]', async () => {
const expected = {
assetId: expect.any(String),
contractId: expect.any(String),
subId: expect.any(String),
icon: expect.any(String),
name: expect.any(String),
symbol: expect.any(String),
decimals: expect.any(Number),
rate: expect.any(Number),
suspicious: expect.any(Boolean),
verified: expect.any(Boolean),
networks: expect.any(Array),
metadata: expect.any(Object),
totalSupply: expect.any(String),
}
const { json } = mockFetch();
json.mockResolvedValueOnce(MOCK_FUEL_ASSET);

const response = await getAssetById({
assetId: MOCK_FUEL_ASSET.assetId
});

const assetInfo = response as AssetInfo;
expect(response).toEqual(MOCK_FUEL_ASSET)
expect(assetInfo).toMatchObject(expected)
expect(Object.keys(assetInfo).sort()).toEqual(Object.keys(expected).sort())
})

it('should get an asset by id [NFT]', async () => {
const expected = {
assetId: expect.any(String),
contractId: expect.any(String),
subId: expect.any(String),

name: expect.any(String),
symbol: expect.any(String),
decimals: expect.any(Number),
totalSupply: expect.any(String),

suspicious: expect.any(Boolean),
verified: expect.any(Boolean),
isNFT: expect.any(Boolean),

metadata: expect.any(Object),
collection: expect.any(String),

owner: expect.any(String),
amount: expect.any(String),
amountInUsd: null,
uri: expect.any(String),
}

const { json } = mockFetch();
json.mockResolvedValueOnce(MOCK_NFT_ASSET);

const response = await getAssetById({
assetId: MOCK_NFT_ASSET.assetId
});

const assetInfo = response as AssetInfo;
expect(response).toEqual(MOCK_NFT_ASSET)
expect(assetInfo).toMatchObject(expected)
expect(Object.keys(assetInfo).sort()).toEqual(Object.keys(expected).sort())
});

it('should use the correct network [mainnet]', async () => {
const { fetch } = mockFetch();
const assetId = '0x0000000000000000000000000000000000000000000000000000000000000000';

await getAssetById({ assetId });

expect(fetch.mock.calls[0][0]).toMatch(`https://mainnet-explorer.fuel.network/assets/${assetId}`);
});

it('should use the correct network [testnet]', async () => {
const { fetch } = mockFetch();
const assetId = '0x0000000000000000000000000000000000000000000000000000000000000000';

await getAssetById({ assetId, network: 'testnet' });

expect(fetch.mock.calls[0][0]).toMatch(`https://explorer-indexer-testnet.fuel.network/assets/${assetId}`);
});

it('should return null if asset not found', async () => {
const { json } = mockFetch();
// The API returns a 200 status code but the response is not valid JSON
json.mockRejectedValueOnce(null);

const response = await getAssetById({
assetId: '0x0000000000000000000000000000000000000000000000000000000000000000'
});

expect(response).toBeNull();
});
});

describe('getAssetByOwner', () => {
it('should get assets by owner', async () => {
const { json } = mockFetch();
json.mockResolvedValueOnce(MOCK_ASSET_INFO_BY_OWNER);

const response = await getAssetsByOwner({
owner: MOCK_NFT_ASSET.owner,
});

expect(response.data).toEqual([MOCK_NFT_ASSET]);
expect(response.pageInfo).toEqual({
count: 1,
})
});


it('should use the correct network [mainnet]', async () => {
const { fetch } = mockFetch();
const owner = '0x0000000000000000000000000000000000000000000000000000000000000000';

await getAssetsByOwner({ owner });

expect(fetch.mock.calls[0][0]).toMatch(`https://mainnet-explorer.fuel.network/accounts/${owner}/assets`);
});

it('should use the correct network [testnet]', async () => {
const { fetch } = mockFetch();
const owner = '0x0000000000000000000000000000000000000000000000000000000000000000';

await getAssetsByOwner({ owner, network: 'testnet' });

expect(fetch.mock.calls[0][0]).toMatch(`https://explorer-indexer-testnet.fuel.network/accounts/${owner}/assets`);
});

it('should return response if no owner is found', async () => {
const { json } = mockFetch();
// The API returns a 200 status code but the response is not valid JSON
json.mockRejectedValueOnce(null);

const response = await getAssetsByOwner({
owner: '0x0000000000000000000000000000000000000000000000000000000000000000'
});

expect(response.data).toEqual([]);
expect(response.pageInfo).toEqual({
count: 0,
})
});
})
});

Loading

0 comments on commit 8030180

Please sign in to comment.