Skip to content

Commit f48c061

Browse files
add: blockscout api and TransactionCard (#31)
1 parent 0b1af78 commit f48c061

37 files changed

+10478
-141
lines changed

Diff for: apps/storybook/src/hooks/use-blockscout.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { asTransactionMeta, getTransaction } from "@/lib/blockscout/api";
2+
import { TransactionMeta } from "@/lib/domain/transaction/transaction";
3+
import { useQuery } from "@tanstack/react-query";
4+
import { Address, Transaction } from "viem";
5+
6+
export const CACHE_KEY = "blockscout";
7+
8+
export const useGetTransaction = (txnHash: string) => {
9+
return useQuery<TransactionMeta>({
10+
queryKey: [`${CACHE_KEY}.transaction`, txnHash],
11+
queryFn: async () => {
12+
const results = await getTransaction(txnHash);
13+
return asTransactionMeta(results);
14+
},
15+
});
16+
};

Diff for: apps/storybook/src/lib/amount.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { formatUnits } from "viem";
2+
3+
export const formatUnitsWithDecimalsDisplayed = (
4+
data: {
5+
value: bigint;
6+
decimals: number;
7+
},
8+
decimalsDisplayed = 4,
9+
) => {
10+
return formatUnits(
11+
data.value / BigInt(Math.pow(10, data?.decimals - decimalsDisplayed)),
12+
decimalsDisplayed,
13+
);
14+
};

Diff for: apps/storybook/src/lib/blockscout/api.test.ts

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { getAddress } from "viem";
2+
import { describe, expect, test } from "vitest";
3+
import { asTransactionMeta, getAddressInfo, getTransaction } from "./api";
4+
import { TXN_VITALIK_DEPOSIT, TXN_VITALIK_TRANSFER } from "./fixture";
5+
6+
describe("blockscout", () => {
7+
test.skip("api", async () => {
8+
const address = "0x962EFc5A602f655060ed83BB657Afb6cc4b5883F";
9+
10+
const results = await getAddressInfo(address);
11+
console.log(results);
12+
const { ens_domain_name } = results;
13+
expect(ens_domain_name).toBe("debuggingfuture.eth");
14+
});
15+
16+
test("getTransaction", async () => {
17+
const txnHash =
18+
"0xc6480de9e7ba4daa2bd115be1aa41c669246b052e6765a4848f8c683c63cacf7";
19+
20+
const { from, to, tx_types } = await getTransaction(txnHash);
21+
expect(from.ens_domain_name).toBe("debuggingfuture.eth");
22+
expect(to.ens_domain_name).toBe(null);
23+
expect(tx_types).toEqual(["coin_transfer"]);
24+
});
25+
});
26+
27+
// transaction_types
28+
29+
describe("Transaction", () => {
30+
test("#asTransactionMeta contract", () => {
31+
const transaction = asTransactionMeta(TXN_VITALIK_DEPOSIT);
32+
33+
expect(transaction.to).toEqual(
34+
"0xB4A8d45647445EA9FC3E1058096142390683dBC2",
35+
);
36+
37+
expect(transaction.displayedTxType).toEqual("contract_call");
38+
39+
expect(transaction.value).toEqual(32010000000000000000000000000n);
40+
expect(transaction.tokenTransfers?.[0]?.name).toEqual("Wrapped Ether");
41+
});
42+
test("#asTransactionMeta transfer", () => {
43+
const transaction = asTransactionMeta(TXN_VITALIK_TRANSFER);
44+
45+
expect(transaction.to).toEqual(
46+
"0x52a785cF0238D02e0F4157735f0a17D04AB2bF6c",
47+
);
48+
49+
expect(transaction.displayedTxType).toEqual("coin_transfer");
50+
51+
expect(transaction.value).toEqual(100000000000000000000000000000n);
52+
expect(transaction.tokenTransfers?.[0]).toEqual([]);
53+
});
54+
});
55+
56+
// {
57+
// block_number_balance_updated_at: 21184382,
58+
// coin_balance: '2084671391552286312',
59+
// creation_transaction_hash: null,
60+
// creation_tx_hash: null,
61+
// creator_address_hash: null,
62+
// ens_domain_name: 'debuggingfuture.eth',
63+
// exchange_rate: '3170.33',
64+
// has_beacon_chain_withdrawals: false,
65+
// has_decompiled_code: false,
66+
// has_logs: false,
67+
// has_token_transfers: true,
68+
// has_tokens: true,
69+
// has_validated_blocks: false,
70+
// hash: '0x962EFc5A602f655060ed83BB657Afb6cc4b5883F',
71+
// implementations: [],
72+
// is_contract: false,
73+
// is_scam: false,
74+
// is_verified: false,
75+
// metadata: null,
76+
// name: null,
77+
// private_tags: [],
78+
// proxy_type: null,
79+
// public_tags: [],
80+
// token: null,
81+
// watchlist_address_id: null,
82+
// watchlist_names: []
83+
// }

Diff for: apps/storybook/src/lib/blockscout/api.ts

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { Address, Transaction, parseGwei, parseUnits } from "viem";
2+
import {
3+
TokenTransfer,
4+
TransactionMeta,
5+
} from "../domain/transaction/transaction";
6+
7+
const ROOT = "https://eth.blockscout.com/api/";
8+
export const invokeApi = async (endpoint: string, body?: any) => {
9+
return fetch(endpoint, {
10+
method: "GET",
11+
headers: {
12+
"Content-Type": "application/json",
13+
},
14+
}).then((res) => {
15+
return res.json();
16+
});
17+
};
18+
19+
// TODO better pairing types
20+
export enum BlockscoutEndpoint {
21+
Transaction = "transaction",
22+
Address = "address",
23+
}
24+
export interface BlockscoutEndpointParams {
25+
address?: Address;
26+
txnHash?: string;
27+
}
28+
29+
export const ENDPOINT_STRATEGIES = {
30+
[BlockscoutEndpoint.Address]: (params: BlockscoutEndpointParams) => {
31+
const { address } = params;
32+
return ROOT + "v1/address/" + address;
33+
},
34+
[BlockscoutEndpoint.Transaction]: (params: BlockscoutEndpointParams) => {
35+
const { txnHash } = params;
36+
return ROOT + "v2/transactions/" + txnHash;
37+
},
38+
};
39+
40+
// TODO enum type
41+
export const getEndpoint = (
42+
endpoint: BlockscoutEndpoint,
43+
params: BlockscoutEndpointParams,
44+
) => {
45+
const strategy = ENDPOINT_STRATEGIES[endpoint];
46+
if (!strategy) {
47+
throw new Error("");
48+
}
49+
50+
return strategy(params);
51+
};
52+
53+
export const getAddressInfo = async (address: Address) => {
54+
const endpoint = getEndpoint(BlockscoutEndpoint.Address, { address });
55+
return invokeApi(endpoint);
56+
};
57+
58+
export const getTransaction = async (txnHash: string) => {
59+
const endpoint = getEndpoint(BlockscoutEndpoint.Transaction, {
60+
txnHash,
61+
});
62+
return invokeApi(endpoint);
63+
};
64+
65+
export const findDisplayedTxType = (transaction_types: any[]): string => {
66+
if (transaction_types.includes("contract_call")) {
67+
return "contract_call";
68+
}
69+
if (transaction_types.includes("coin_transfer")) {
70+
return "coin_transfer";
71+
}
72+
return "native_transfer";
73+
};
74+
75+
export const asTokenTransfer = (transfer: any): TokenTransfer => {
76+
const { token } = transfer;
77+
return {
78+
...token,
79+
// imageUrl: '',
80+
name: transfer.token.name,
81+
amount: parseUnits(transfer.total.value, transfer.total.decimals),
82+
};
83+
};
84+
85+
/**
86+
*
87+
* Requires chain specific info for native currency symbol
88+
*
89+
*/
90+
export const asTransactionMeta = (res: any): TransactionMeta => {
91+
return {
92+
hash: res.hash,
93+
blockHash: res.blockHash,
94+
from: res.from.hash as Address,
95+
to: res.to.hash as Address,
96+
isSuccess: res.success,
97+
displayedTxType: findDisplayedTxType(res.tx_types),
98+
value: parseUnits(res.value, res.decimals),
99+
tokenTransfers: res.token_transfers.map(asTokenTransfer),
100+
};
101+
};

Diff for: apps/storybook/src/lib/blockscout/chain.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// explorer url in chainInfo.ts are pulled from https://github.com/blockscout/chainscout/blob/main/data/chains.json
2+
// https://www.blockscout.com/chains-and-projects
3+
4+
import { Chain } from "viem";
5+
import { BLOCKSCOUT_CHAINS_BY_ID } from "./chainInfo";
6+
7+
// TODO ensure treeshaking
8+
9+
export const getBlockscoutChainEndpoint = (chain: Chain) => {
10+
const blockScoutChainExplorer =
11+
BLOCKSCOUT_CHAINS_BY_ID[chain.id.toString()]?.explorers?.[0];
12+
13+
if (!blockScoutChainExplorer) {
14+
return;
15+
}
16+
17+
return blockScoutChainExplorer.url;
18+
};

0 commit comments

Comments
 (0)