Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add StarkScan client to fetch transaction history #341

Merged
merged 25 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
77f4b7b
feat: add stark scan client
stanleyyconsensys Sep 2, 2024
6b7a5e2
chore: add starkscan config
stanleyyconsensys Sep 2, 2024
3c9a523
chore: lint
stanleyyconsensys Sep 2, 2024
576f302
chore: add interface
stanleyyconsensys Sep 2, 2024
19fdf56
Merge branch 'main' into feat/add-stark-scan-client
stanleyyconsensys Sep 5, 2024
5d446cb
chore: support multiple txn
stanleyyconsensys Sep 5, 2024
3dbdf32
chore: update starkscan
stanleyyconsensys Sep 5, 2024
924ea45
chore: update stark scan client
stanleyyconsensys Sep 6, 2024
bcf34c7
chore: update contract func name
stanleyyconsensys Sep 6, 2024
d1ad70c
chore: fix test
stanleyyconsensys Sep 6, 2024
1a061d3
chore: update data client
stanleyyconsensys Sep 6, 2024
3bda85f
Merge branch 'main' into feat/add-stark-scan-client
stanleyyconsensys Sep 6, 2024
114ea6d
Merge branch 'main' into feat/add-stark-scan-client
stanleyyconsensys Oct 22, 2024
7539056
Merge branch 'main' into feat/add-stark-scan-client
stanleyyconsensys Dec 2, 2024
ed699ff
chore: re-structure starkscan type
stanleyyconsensys Dec 2, 2024
93adb36
chore: add test coverage
stanleyyconsensys Dec 2, 2024
dadb493
chore: factory and config
stanleyyconsensys Dec 2, 2024
b21a5d2
chore: add backward compatibility for transactions type
stanleyyconsensys Dec 2, 2024
07f0232
chore: add comment
stanleyyconsensys Dec 2, 2024
7a26c70
chore: lint
stanleyyconsensys Dec 2, 2024
804a2bd
chore: resolve review comment
stanleyyconsensys Dec 4, 2024
8e9e163
chore: change dataVersion to enum
stanleyyconsensys Dec 4, 2024
b09361f
chore: lint
stanleyyconsensys Dec 4, 2024
63e3030
chore: update starkscan to handle missing selector_name
stanleyyconsensys Dec 5, 2024
24bfb38
Merge branch 'main' into feat/add-stark-scan-client
khanti42 Dec 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 ?? '',
khanti42 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading