Skip to content

Commit

Permalink
feat: add StarkScan client to fetch transaction history (#341)
Browse files Browse the repository at this point in the history
* feat: add stark scan client

* chore: add starkscan config

* chore: lint

* chore: add interface

* chore: support multiple txn

* chore: update starkscan

* chore: update stark scan client

* chore: update contract func name

* chore: fix test

* chore: update data client

* chore: re-structure starkscan type

* chore: add test coverage

* chore: factory and config

* chore: add backward compatibility for transactions type

* chore: add comment

* chore: lint

* chore: resolve review comment

* chore: change dataVersion to enum

* chore: lint

* chore: update starkscan to handle missing selector_name

---------

Co-authored-by: khanti42 <[email protected]>
  • Loading branch information
stanleyyconsensys and khanti42 authored Dec 5, 2024
1 parent c9e2c64 commit cfdc79d
Show file tree
Hide file tree
Showing 13 changed files with 1,314 additions and 4 deletions.
3 changes: 3 additions & 0 deletions packages/starknet-snap/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ SNAP_ENV=dev
# Description: Environment variables for API key of VOYAGER
# Required: false
VOYAGER_API_KEY=
# Description: Environment variables for API key of STARKSCAN
# Required: false
STARKSCAN_API_KEY=
# Description: Environment variables for API key of ALCHEMY
# Required: false
ALCHEMY_API_KEY=
Expand Down
1 change: 1 addition & 0 deletions packages/starknet-snap/snap.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const config: SnapConfig = {
SNAP_ENV: process.env.SNAP_ENV ?? 'prod',
VOYAGER_API_KEY: process.env.VOYAGER_API_KEY ?? '',
ALCHEMY_API_KEY: process.env.ALCHEMY_API_KEY ?? '',
STARKSCAN_API_KEY: process.env.STARKSCAN_API_KEY ?? '',
LOG_LEVEL: process.env.LOG_LEVEL ?? '0',
/* eslint-disable */
},
Expand Down
163 changes: 163 additions & 0 deletions packages/starknet-snap/src/__tests__/fixture/stark-scan-example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
{
"getTransactionsResp": {
"next_url": null,
"data": []
},
"invokeTx": {
"transaction_hash": "0x06d05d58dc35f432f8f5c7ae5972434a00d2e567e154fc1226c444f06e369c7d",
"block_hash": "0x02a44fd749221729c63956309d0df4ba03f4291613248d885870b55baf963e0e",
"block_number": 136140,
"transaction_index": 6,
"transaction_status": "ACCEPTED_ON_L1",
"transaction_finality_status": "ACCEPTED_ON_L1",
"transaction_execution_status": "SUCCEEDED",
"transaction_type": "INVOKE_FUNCTION",
"version": 1,
"signature": [
"0x555fe1b8e5183be2f6c81e5203ee3928aab894ab0b31279c89a3c7f016865fc",
"0x269d0a83634905be76372d3116733afc8a8f0f29776f57d7400b05ded54c9b1"
],
"max_fee": "95250978959328",
"actual_fee": "62936888346418",
"nonce": "9",
"contract_address": null,
"entry_point_selector": null,
"entry_point_type": null,
"calldata": [
"0x1",
"0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
"0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e",
"0x3",
"0x42ae4bf23ef08090010662d275982b6070edfecb131e9fb83dfc1414d226529",
"0x9184e72a000",
"0x0"
],
"class_hash": null,
"sender_address": "0x042ae4bf23ef08090010662d275982b6070edfecb131e9fb83dfc1414d226529",
"constructor_calldata": null,
"contract_address_salt": null,
"timestamp": 1724759407,
"entry_point_selector_name": "__execute__",
"number_of_events": 3,
"revert_error": null,
"account_calls": [
{
"block_hash": "0x02a44fd749221729c63956309d0df4ba03f4291613248d885870b55baf963e0e",
"block_number": 136140,
"transaction_hash": "0x06d05d58dc35f432f8f5c7ae5972434a00d2e567e154fc1226c444f06e369c7d",
"caller_address": "0x042ae4bf23ef08090010662d275982b6070edfecb131e9fb83dfc1414d226529",
"contract_address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
"calldata": [
"0x42ae4bf23ef08090010662d275982b6070edfecb131e9fb83dfc1414d226529",
"0x9184e72a000",
"0x0"
],
"result": ["0x1"],
"timestamp": 1724759407,
"call_type": "CALL",
"class_hash": "0x07f3777c99f3700505ea966676aac4a0d692c2a9f5e667f4c606b51ca1dd3420",
"selector": "0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e",
"entry_point_type": "EXTERNAL",
"selector_name": "transfer"
}
]
},
"upgradeTx": {
"transaction_hash": "0x019a7a7bbda2e52f82ffc867488cace31a04d9340ad56bbe9879aab8bc47f0b6",
"block_hash": "0x002dae3ed3cf7763621da170103384d533ed09fb987a232f23b7d8febbbca67f",
"block_number": 77586,
"transaction_index": 33,
"transaction_status": "ACCEPTED_ON_L1",
"transaction_finality_status": "ACCEPTED_ON_L1",
"transaction_execution_status": "SUCCEEDED",
"transaction_type": "INVOKE_FUNCTION",
"version": 1,
"signature": [
"0x417671c63219250e0c80d53b1e1b3c0dd76ade552806a51fdfd8c06f7c47a12",
"0x91c7ccadec2ba22bfa5c92b62fc6eaccb56c686279f953c5012f7d6f679570"
],
"max_fee": "191210494208472",
"actual_fee": "148188646762488",
"nonce": "4",
"contract_address": null,
"entry_point_selector": null,
"entry_point_type": null,
"calldata": [
"0x1",
"0x42ae4bf23ef08090010662d275982b6070edfecb131e9fb83dfc1414d226529",
"0xf2f7c15cbe06c8d94597cd91fd7f3369eae842359235712def5584f8d270cd",
"0x0",
"0x3",
"0x3",
"0x29927c8af6bccf3f6fda035981e765a7bdbf18a2dc0d630494f8758aa908e2b",
"0x1",
"0x0"
],
"class_hash": null,
"sender_address": "0x042ae4bf23ef08090010662d275982b6070edfecb131e9fb83dfc1414d226529",
"constructor_calldata": null,
"contract_address_salt": null,
"timestamp": 1719830196,
"entry_point_selector_name": "__execute__",
"number_of_events": 4,
"revert_error": null,
"account_calls": [
{
"block_hash": "0x002dae3ed3cf7763621da170103384d533ed09fb987a232f23b7d8febbbca67f",
"block_number": 77586,
"transaction_hash": "0x019a7a7bbda2e52f82ffc867488cace31a04d9340ad56bbe9879aab8bc47f0b6",
"caller_address": "0x042ae4bf23ef08090010662d275982b6070edfecb131e9fb83dfc1414d226529",
"contract_address": "0x042ae4bf23ef08090010662d275982b6070edfecb131e9fb83dfc1414d226529",
"calldata": [
"0x29927c8af6bccf3f6fda035981e765a7bdbf18a2dc0d630494f8758aa908e2b",
"0x1",
"0x0"
],
"result": ["0x1", "0x0"],
"timestamp": 1719830196,
"call_type": "CALL",
"class_hash": "0x025ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918",
"selector": "0xf2f7c15cbe06c8d94597cd91fd7f3369eae842359235712def5584f8d270cd",
"entry_point_type": "EXTERNAL",
"selector_name": "upgrade"
}
]
},
"cairo0DeployTx": {
"transaction_hash": "0x06210d8004e1c90723732070c191a3a003f99d1d95e6c7766322ed75d9d83d78",
"block_hash": "0x058a67093c5f642a7910b7aef0c0a846834e1df60f9bf4c0564afb9c8efe3a41",
"block_number": 68074,
"transaction_index": 6,
"transaction_status": "ACCEPTED_ON_L1",
"transaction_finality_status": "ACCEPTED_ON_L1",
"transaction_execution_status": "SUCCEEDED",
"transaction_type": "DEPLOY_ACCOUNT",
"version": 1,
"signature": [
"0x2de38508b633161a3cdbc0a04b0e09f85c884254552f903417239f95486ceda",
"0x2694930b199802941c996f8aaf48e63a1b2e51ccfaec7864f83f40fcd285286"
],
"max_fee": "6639218055204",
"actual_fee": "21040570099",
"nonce": null,
"contract_address": "0x042ae4bf23ef08090010662d275982b6070edfecb131e9fb83dfc1414d226529",
"entry_point_selector": null,
"entry_point_type": null,
"calldata": null,
"class_hash": "0x025ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918",
"sender_address": null,
"constructor_calldata": [
"0x33434ad846cdd5f23eb73ff09fe6fddd568284a0fb7d1be20ee482f044dabe2",
"0x79dc0da7c54b95f10aa182ad0a46400db63156920adb65eca2654c0945a463",
"0x2",
"0xbd7fccd6d25df79e3fc8dd539efd03fe448d902b8bc5955e60b3830988ce50",
"0x0"
],
"contract_address_salt": "334816139481647544515869631733577866188380288661138191555306848313001168464",
"timestamp": 1716355916,
"entry_point_selector_name": "constructor",
"number_of_events": 2,
"revert_error": null,
"account_calls": []
}
}
64 changes: 64 additions & 0 deletions packages/starknet-snap/src/__tests__/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import {
} from 'starknet';
import { v4 as uuidv4 } from 'uuid';

import type {
StarkScanTransaction,
StarkScanTransactionsResponse,
} from '../chain/data-client/starkscan.type';
import { FeeToken } from '../types/snapApi';
import type {
AccContract,
Expand All @@ -32,6 +36,7 @@ import {
PROXY_CONTRACT_HASH,
} from '../utils/constants';
import { grindKey } from '../utils/keyPair';
import { invokeTx, cairo0DeployTx } from './fixture/stark-scan-example.json';

/* eslint-disable */
export type StarknetAccount = AccContract & {
Expand Down Expand Up @@ -364,6 +369,65 @@ export function generateTransactionRequests({
return requests;
}

/**
* Method to generate starkscan transactions.
*
* @param params
* @param params.address - Address of the account.
* @param params.startFrom - start timestamp of the first transactions.
* @param params.timestampReduction - the deduction timestamp per transactions.
* @param params.txnTypes - Array of txn types.
* @param params.cnt - Number of transaction to generate.
* @returns An array of transaction object.
*/
export function generateStarkScanTransactions({
address,
startFrom = Date.now(),
timestampReduction = 100,
cnt = 10,
txnTypes = [TransactionType.DEPLOY_ACCOUNT, TransactionType.INVOKE],
}: {
address: string;
startFrom?: number;
timestampReduction?: number;
cnt?: number;
txnTypes?: TransactionType[];
}): StarkScanTransactionsResponse {
let transactionStartFrom = startFrom;
const txs: StarkScanTransaction[] = [];
let totalRecordCnt = txnTypes.includes(TransactionType.DEPLOY_ACCOUNT)
? cnt - 1
: cnt;

for (let i = 0; i < totalRecordCnt; i++) {
let newTx = {
...invokeTx,
account_calls: [...invokeTx.account_calls],
};
newTx.sender_address = address;
newTx.account_calls[0].caller_address = address;
newTx.timestamp = transactionStartFrom;
newTx.transaction_hash = `0x${transactionStartFrom.toString(16)}`;
transactionStartFrom -= timestampReduction;
txs.push(newTx as unknown as StarkScanTransaction);
}

if (txnTypes.includes(TransactionType.DEPLOY_ACCOUNT)) {
let deployTx = {
...cairo0DeployTx,
account_calls: [...cairo0DeployTx.account_calls],
};
deployTx.contract_address = address;
deployTx.transaction_hash = `0x${transactionStartFrom.toString(16)}`;
txs.push(deployTx as unknown as StarkScanTransaction);
}

return {
next_url: null,
data: txs,
};
}

/**
* Method to generate a mock estimate fee response.
*
Expand Down
130 changes: 130 additions & 0 deletions packages/starknet-snap/src/chain/api-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import type { Json } from '@metamask/snaps-sdk';
import type { Struct } from 'superstruct';
import { mask } from 'superstruct';

import { logger } from '../utils/logger';

export enum HttpMethod {
Get = 'GET',
Post = 'POST',
}

export type HttpHeaders = Record<string, string>;

export type HttpRequest = {
url: string;
method: HttpMethod;
headers: HttpHeaders;
body?: string;
};

export type HttpResponse = globalThis.Response;

export abstract class ApiClient {
/**
* The name of the API Client.
*/
abstract apiClientName: string;

/**
* An internal method called internally by `submitRequest()` to verify and convert the HTTP response to the expected API response.
*
* @param response - The HTTP response to verify and convert.
* @returns A promise that resolves to the API response.
*/
protected async parseResponse<ApiResponse>(
response: HttpResponse,
): Promise<ApiResponse> {
try {
return (await response.json()) as unknown as ApiResponse;
} catch (error) {
throw new Error(
'API response error: response body can not be deserialised.',
);
}
}

/**
* An internal method used to build the `HttpRequest` object.
*
* @param params - The request parameters.
* @param params.method - The HTTP method (GET or POST).
* @param params.headers - The HTTP headers.
* @param params.url - The request URL.
* @param [params.body] - The request body (optional).
* @returns A `HttpRequest` object.
*/
protected buildHttpRequest({
method,
headers = {},
url,
body,
}: {
method: HttpMethod;
headers?: HttpHeaders;
url: string;
body?: Json;
}): HttpRequest {
const request = {
url,
method,
headers: {
'Content-Type': 'application/json',
...headers,
},
body:
method === HttpMethod.Post && body ? JSON.stringify(body) : undefined,
};

return request;
}

/**
* An internal method used to send a HTTP request.
*
* @param params - The request parameters.
* @param [params.requestName] - The name of the request (optional).
* @param params.request - The `HttpRequest` object.
* @param params.responseStruct - The superstruct used to verify the API response.
* @returns A promise that resolves to a JSON object.
*/
protected async sendHttpRequest<ApiResponse>({
requestName = '',
request,
responseStruct,
}: {
requestName?: string;
request: HttpRequest;
responseStruct: Struct;
}): Promise<ApiResponse> {
const logPrefix = `[${this.apiClientName}.${requestName}]`;

try {
logger.debug(`${logPrefix} request: ${request.method}`); // Log HTTP method being used.

const fetchRequest = {
method: request.method,
headers: request.headers,
body: request.body,
};

const httpResponse = await fetch(request.url, fetchRequest);

const jsonResponse = await this.parseResponse<ApiResponse>(httpResponse);

logger.debug(`${logPrefix} response:`, JSON.stringify(jsonResponse));

// Safeguard to identify if the response has some unexpected changes from the API client
mask(jsonResponse, responseStruct, `Unexpected response from API client`);

return jsonResponse;
} catch (error) {
logger.info(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${logPrefix} error: ${error.message}`,
);

throw error;
}
}
}
Loading

0 comments on commit cfdc79d

Please sign in to comment.