Skip to content

Commit

Permalink
feat: waitForTransaction tx hash readiness, test errors & logging upd…
Browse files Browse the repository at this point in the history
…ate, rpc
  • Loading branch information
tabaktoni committed Nov 10, 2023
1 parent 054be8b commit 39b3fa7
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 37 deletions.
2 changes: 1 addition & 1 deletion __tests__/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const getTestProvider = (): ProviderInterface => {
? new RpcProvider({ nodeUrl: process.env.TEST_RPC_URL })
: new SequencerProvider({ baseUrl: process.env.TEST_PROVIDER_BASE_URL || '' });

if (process.env.IS_LOCALHOST_DEVNET) {
if (process.env.IS_LOCALHOST_DEVNET === 'true') {
// accelerate the tests when running locally
const originalWaitForTransaction = provider.waitForTransaction.bind(provider);
provider.waitForTransaction = (
Expand Down
21 changes: 10 additions & 11 deletions __tests__/rpcProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ describeIfRpc('RPCProvider', () => {
const rpcProvider = getTestProvider() as RpcProvider;
const account = getTestAccount(rpcProvider);
let accountPublicKey: string;
initializeMatcher(expect);

beforeAll(async () => {
initializeMatcher(expect);
expect(account).toBeInstanceOf(Account);
const accountKeyPair = utils.randomPrivateKey();
accountPublicKey = getStarkKey(accountKeyPair);
Expand Down Expand Up @@ -92,10 +92,9 @@ describeIfRpc('RPCProvider', () => {
});
expect(estimation).toEqual(
expect.objectContaining({
overall_fee: expect.anything(),
gas_consumed: expect.anything(),
gas_price: expect.anything(),
gas_usage: expect.anything(),
unit: 'wei',
overall_fee: expect.anything(),
})
);
});
Expand Down Expand Up @@ -206,13 +205,6 @@ describeIfRpc('RPCProvider', () => {
});
});

describeIfNotDevnet('testnet only', () => {
test('getSyncingStats', async () => {
const syncingStats = await rpcProvider.getSyncingStats();
expect(syncingStats).toMatchSchemaRef('GetSyncingStatsResponse');
});
});

describe('deploy contract related tests', () => {
let contract_address: string;
let transaction_hash: string;
Expand Down Expand Up @@ -269,4 +261,11 @@ describeIfRpc('RPCProvider', () => {
});
});
});

describeIfNotDevnet('global rpc only', () => {
test('getSyncingStats', async () => {
const syncingStats = await rpcProvider.getSyncingStats();
expect(syncingStats).toMatchSchemaRef('GetSyncingStatsResponse');
});
});
});
3 changes: 2 additions & 1 deletion __tests__/utils/json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ const bi = (x: bigint) => BigInt(Number.MAX_SAFE_INTEGER) + x;
const objString = `{"constructor":"c","n0":${bi(0n)},"n1":${bi(1n)},"n2":12,"n3":1.1}`;

describe('JSON utility tests', () => {
test('stringify', () => {
xtest('stringify', () => {
// Disabled due to random nodeUrl nad diff params for RPC
expect(stringify(defaultProvider)).toMatchSnapshot();
});

Expand Down
2 changes: 1 addition & 1 deletion src/provider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ export * from './sequencer';
export * from './interface';
export * from './rpc';

export const defaultProvider = new Provider();
export const defaultProvider = new Provider({ rpc: { default: true } });
82 changes: 60 additions & 22 deletions src/provider/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ import { CallData } from '../utils/calldata';
import { getAbiContractVersion } from '../utils/calldata/cairo';
import { isSierra } from '../utils/contract';
import fetch from '../utils/fetchPonyfill';
import { getSelectorFromName, getVersionsByType } from '../utils/hash';
import { getSelector, getSelectorFromName, getVersionsByType } from '../utils/hash';
import { stringify } from '../utils/json';
import { toHex, toStorageKey } from '../utils/num';
import { getHexStringArray, toHex, toStorageKey } from '../utils/num';
import { wait } from '../utils/provider';
import { RPCResponseParser } from '../utils/responseParser/rpc';
import { decompressProgram, signatureToHexArray } from '../utils/stark';
Expand All @@ -42,9 +42,10 @@ import { ProviderInterface } from './interface';
import { getAddressFromStarkName, getStarkName } from './starknetId';
import { Block } from './utils';

export const getDefaultNodeUrl = (networkName?: NetworkName): string => {
// eslint-disable-next-line no-console
console.warn('Using default public node url, please provide nodeUrl in provider options!');
export const getDefaultNodeUrl = (networkName?: NetworkName, mute: boolean = false): string => {
if (!mute)
// eslint-disable-next-line no-console
console.warn('Using default public node url, please provide nodeUrl in provider options!');
const nodes = networkName === NetworkName.SN_MAIN ? RPC_MAINNET_NODES : RPC_GOERLI_NODES;
const randIdx = Math.floor(Math.random() * nodes.length);
return nodes[randIdx];
Expand Down Expand Up @@ -73,13 +74,13 @@ export class RpcProvider implements ProviderInterface {
const { nodeUrl, retries, headers, blockIdentifier, chainId } = optionsOrProvider || {};
if (Object.values(NetworkName).includes(nodeUrl as NetworkName)) {
// Network name provided for nodeUrl
this.nodeUrl = getDefaultNodeUrl(nodeUrl as NetworkName);
this.nodeUrl = getDefaultNodeUrl(nodeUrl as NetworkName, optionsOrProvider?.default);
} else if (nodeUrl) {
// NodeUrl provided
this.nodeUrl = nodeUrl;
} else {
// none provided fallback to default testnet
this.nodeUrl = getDefaultNodeUrl();
this.nodeUrl = getDefaultNodeUrl(undefined, optionsOrProvider?.default);
}
this.retries = retries || defaultOptions.retries;
this.headers = { ...defaultOptions.headers, ...headers };
Expand All @@ -89,18 +90,28 @@ export class RpcProvider implements ProviderInterface {
}

public fetch(method: string, params?: object, id: string | number = 0) {
const rpcRequestBody: RPC.JRPC.RequestBody = { id, jsonrpc: '2.0', method, params };
const rpcRequestBody: RPC.JRPC.RequestBody = {
id,
jsonrpc: '2.0',
method,
...(params && { params }),
};
return fetch(this.nodeUrl, {
method: 'POST',
body: stringify(rpcRequestBody),
headers: this.headers as Record<string, string>,
});
}

protected errorHandler(rpcError?: RPC.JRPC.Error, otherError?: any) {
protected errorHandler(method: string, params: any, rpcError?: RPC.JRPC.Error, otherError?: any) {
if (rpcError) {
const { code, message, data } = rpcError;
throw new LibraryError(`${code}: ${message}: ${data}`);
throw new LibraryError(
`RPC: ${method} with params ${JSON.stringify(params)}\n ${code}: ${message}: ${data}`
);
}
if (otherError instanceof LibraryError) {
throw otherError;
}
if (otherError) {
throw Error(otherError.message);
Expand All @@ -114,10 +125,10 @@ export class RpcProvider implements ProviderInterface {
try {
const rawResult = await this.fetch(method, params);
const { error, result } = await rawResult.json();
this.errorHandler(error);
this.errorHandler(method, params, error);
return result as RPC.Methods[T]['result'];
} catch (error: any) {
this.errorHandler(error?.response?.data, error);
this.errorHandler(method, params, error?.response?.data, error);
throw error;
}
}
Expand Down Expand Up @@ -310,15 +321,17 @@ export class RpcProvider implements ProviderInterface {
let onchain = false;
let isErrorState = false;
const retryInterval = options?.retryInterval ?? 5000;
const errorStates: any = options?.errorStates ?? [RPC.ETransactionExecutionStatus.REVERTED];
const errorStates: any = options?.errorStates ?? [
RPC.ETransactionStatus.REJECTED,
RPC.ETransactionExecutionStatus.REVERTED,
];
const successStates: any = options?.successStates ?? [
RPC.ETransactionExecutionStatus.SUCCEEDED,
RPC.ETransactionFinalityStatus.ACCEPTED_ON_L1,
RPC.ETransactionFinalityStatus.ACCEPTED_ON_L2,
RPC.ETransactionStatus.ACCEPTED_ON_L2,
RPC.ETransactionStatus.ACCEPTED_ON_L1,
];

let txStatus: RPC.TransactionStatus;

while (!onchain) {
// eslint-disable-next-line no-await-in-loop
await wait(retryInterval);
Expand All @@ -329,8 +342,8 @@ export class RpcProvider implements ProviderInterface {
const executionStatus = txStatus.execution_status;
const finalityStatus = txStatus.finality_status;

if (!executionStatus || !finalityStatus) {
// Transaction is potentially REJECTED or NOT_RECEIVED but RPC doesn't have those statuses
if (!finalityStatus) {
// Transaction is potentially NOT_RECEIVED or RPC not Synced yet
// so we will retry '{ retries }' times
const error = new Error('waiting for transaction status');
throw error;
Expand All @@ -350,16 +363,33 @@ export class RpcProvider implements ProviderInterface {
throw error;
}

if (retries === 0) {
if (retries <= 0) {
throw new Error(`waitForTransaction timed-out with retries ${this.retries}`);
}
}

retries -= 1;
}

await wait(retryInterval);
return this.getTransactionReceipt(transactionHash);
/**
* Nodes even doe transaction is executionStatus SUCCEEDED finalityStatus ACCEPTED_ON_L2, getTransactionReceipt return Transaction hash not found
* Retry until rpc is actually ready to work with txHash
*/
let txReceipt = null;
while (txReceipt === null) {
try {
// eslint-disable-next-line no-await-in-loop
txReceipt = await this.getTransactionReceipt(transactionHash);
} catch (error) {
if (retries <= 0) {
throw new Error(`waitForTransaction timed-out with retries ${this.retries}`);
}
}
retries -= 1;
// eslint-disable-next-line no-await-in-loop
await wait(retryInterval);
}
return txReceipt as RPC.TransactionReceipt;
}

public async getStorageAt(
Expand Down Expand Up @@ -642,9 +672,17 @@ export class RpcProvider implements ProviderInterface {
message: RPC.L1Message,
blockIdentifier: BlockIdentifier = this.blockIdentifier
) {
const { from_address, to_address, entry_point_selector, payload } = message;
const formattedMessage = {
from_address: toHex(from_address),
to_address: toHex(to_address),
entry_point_selector: getSelector(entry_point_selector),
payload: getHexStringArray(payload),
};

const block_id = new Block(blockIdentifier).identifier;
return this.fetchEndpoint('starknet_estimateMessageFee', {
message,
message: formattedMessage,
block_id,
});
}
Expand Down
9 changes: 8 additions & 1 deletion src/types/api/rpcspec/nonspec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export type ChainId = CHAIN_ID;
export type Transaction = TXN;
export type TransactionStatus = {
finality_status: TXN_STATUS;
execution_status: TXN_EXECUTION_STATUS;
execution_status?: TXN_EXECUTION_STATUS;
};

// Diff Than Seq
Expand All @@ -107,6 +107,13 @@ export enum ESimulationFlag {
SKIP_FEE_CHARGE = 'SKIP_FEE_CHARGE',
}

export enum ETransactionStatus {
RECEIVED = 'RECEIVED',
REJECTED = 'REJECTED',
ACCEPTED_ON_L2 = 'ACCEPTED_ON_L2',
ACCEPTED_ON_L1 = 'ACCEPTED_ON_L1',
}

export enum ETransactionFinalityStatus {
ACCEPTED_ON_L2 = 'ACCEPTED_ON_L2',
ACCEPTED_ON_L1 = 'ACCEPTED_ON_L1',
Expand Down
1 change: 1 addition & 0 deletions src/types/provider/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type RpcProviderOptions = {
headers?: object;
blockIdentifier?: BlockIdentifier;
chainId?: StarknetChainId;
default?: boolean;
};

export type SequencerHttpMethod = 'POST' | 'GET';
Expand Down

0 comments on commit 39b3fa7

Please sign in to comment.