@atomiqlabs/chain-starknet
Version:
Starknet specific base implementation
335 lines (294 loc) • 11 kB
text/typescript
import {Provider, constants, stark, ec, Account, provider, wallet, WebSocketChannel, logger} from "starknet";
import {calculateHash, getLogger, toHex} from "../../utils/Utils";
import {SignedStarknetTx, StarknetTransactions, StarknetTx} from "./modules/StarknetTransactions";
import {StarknetFees} from "./modules/StarknetFees";
import {StarknetAddresses} from "./modules/StarknetAddresses";
import {StarknetTokens} from "./modules/StarknetTokens";
import {StarknetEvents} from "./modules/StarknetEvents";
import {StarknetSignatures} from "./modules/StarknetSignatures";
import {StarknetAccounts} from "./modules/StarknetAccounts";
import {StarknetBlocks} from "./modules/StarknetBlocks";
import {BitcoinNetwork, ChainInterface, TransactionConfirmationOptions} from "@atomiqlabs/base";
import {StarknetSigner} from "../wallet/StarknetSigner";
import {Buffer} from "buffer";
import {StarknetKeypairWallet} from "../wallet/accounts/StarknetKeypairWallet";
import {StarknetBrowserSigner} from "../wallet/StarknetBrowserSigner";
/**
* Configuration options for Starknet chain interface
*
* @category Chain Interface
*/
export type StarknetConfig = {
/**
* Limit of the number of events retrieved by a single `starknet_getEvents` RPC call.
*
* Defaults to 100 events
*/
getLogChunkSize?: number, //100
/**
* When fetching events in the forward direction, sets the limit on the number of blocks
* to fetch in a single `starknet_getEvents` RPC call.
*
* Defaults to 2000 blocks
*/
getLogForwardBlockRange?: number, //2000
/**
* Maximum numbers of keys allowed to be specified in a single `starknet_getEvents` RPC call
*
* Defaults to 64 keys
*/
maxGetLogKeys?: number, //64
/**
* Maximum number of parallel contract calls to execute in batch functions
*/
maxParallelCalls?: number, //10
};
/**
* Main chain interface for interacting with Starknet blockchain
*
* @category Chain Interface
*/
export class StarknetChainInterface implements ChainInterface<StarknetTx, SignedStarknetTx, StarknetSigner, "STARKNET", Account> {
public readonly chainId = "STARKNET";
public readonly starknetChainId: constants.StarknetChainId;
/**
* Optional websocket channel for instant notifications
* @internal
*/
readonly wsChannel?: WebSocketChannel;
/**
* Underlying starknet.js provider
* @internal
*/
readonly provider: Provider;
public Fees: StarknetFees;
public readonly Tokens: StarknetTokens;
public readonly Transactions: StarknetTransactions;
public readonly Signatures: StarknetSignatures;
public readonly Events: StarknetEvents;
public readonly Accounts: StarknetAccounts;
public readonly Blocks: StarknetBlocks;
public readonly config: StarknetConfig;
private readonly bitcoinNetwork?: BitcoinNetwork;
constructor(
chainId: constants.StarknetChainId,
provider: Provider,
wsChannel?: WebSocketChannel,
feeEstimator: StarknetFees = new StarknetFees(provider),
options?: StarknetConfig,
bitcoinNetwork?: BitcoinNetwork
) {
this.starknetChainId = chainId;
this.provider = provider;
this.config = options ?? {};
this.config.getLogForwardBlockRange ??= 2000;
this.config.getLogChunkSize ??= 100;
this.config.maxGetLogKeys ??= 64;
this.config.maxParallelCalls ??= 10;
this.wsChannel = wsChannel;
this.Fees = feeEstimator;
this.Tokens = new StarknetTokens(this);
this.Transactions = new StarknetTransactions(this);
this.Signatures = new StarknetSignatures(this);
this.Events = new StarknetEvents(this);
this.Accounts = new StarknetAccounts(this);
this.Blocks = new StarknetBlocks(this);
this.bitcoinNetwork = bitcoinNetwork;
}
/**
* @inheritDoc
*/
async getBalance(signer: string, tokenAddress: string): Promise<bigint> {
//TODO: For native token we should discount the cost of deploying an account if it is not deployed yet and the tx fee
return await this.Tokens.getTokenBalance(signer, tokenAddress);
}
/**
* @inheritDoc
*/
getNativeCurrencyAddress(): string {
return this.Tokens.getNativeCurrencyAddress();
}
/**
* @inheritDoc
*/
isValidToken(tokenIdentifier: string): boolean {
return this.Tokens.isValidToken(tokenIdentifier);
}
/**
* @inheritDoc
*/
isValidAddress(address: string, lenient?: boolean): boolean {
return StarknetAddresses.isValidAddress(address, lenient);
}
/**
* @inheritDoc
*/
normalizeAddress(address: string): string {
return toHex(address);
}
///////////////////////////////////
//// Callbacks & handlers
/**
* @inheritDoc
*/
offBeforeTxReplace(callback: (oldTx: string, oldTxId: string, newTx: string, newTxId: string) => Promise<void>): boolean {
return true;
}
/**
* @inheritDoc
*/
onBeforeTxReplace(callback: (oldTx: string, oldTxId: string, newTx: string, newTxId: string) => Promise<void>): void {}
/**
* @inheritDoc
*/
onBeforeTxSigned(callback: (tx: StarknetTx) => Promise<void>): void {
this.Transactions.onBeforeTxSigned(callback);
}
/**
* @inheritDoc
*/
offBeforeTxSigned(callback: (tx: StarknetTx) => Promise<void>): boolean {
return this.Transactions.offBeforeTxSigned(callback);
}
/**
* @inheritDoc
*/
randomAddress(): string {
return toHex(stark.randomAddress());
}
/**
* @inheritDoc
*/
randomSigner(): StarknetSigner {
const privateKey = "0x"+Buffer.from(ec.starkCurve.utils.randomPrivateKey()).toString("hex");
const wallet = new StarknetKeypairWallet(this.provider, privateKey);
return new StarknetSigner(wallet);
}
////////////////////////////////////////////
//// Transactions
/**
* @inheritDoc
*/
sendAndConfirm(
signer: StarknetSigner,
txs: StarknetTx[],
waitForConfirmation?: boolean,
abortSignal?: AbortSignal,
parallel?: boolean,
onBeforePublish?: (txId: string, rawTx: string) => Promise<void>
): Promise<string[]> {
return this.Transactions.sendAndConfirm(signer, txs, waitForConfirmation, abortSignal, parallel, onBeforePublish);
}
/**
* @inheritDoc
*/
sendSignedAndConfirm(
signedTxs: SignedStarknetTx[],
waitForConfirmation?: boolean,
abortSignal?: AbortSignal,
parallel?: boolean,
onBeforePublish?: (txId: string, rawTx: string) => Promise<void>
): Promise<string[]> {
return this.Transactions.sendSignedAndConfirm(signedTxs, waitForConfirmation, abortSignal, parallel, onBeforePublish);
}
/**
* @inheritDoc
*/
async prepareTxs(txs: StarknetTx[]): Promise<StarknetTx[]> {
await this.Transactions.prepareTransactions(txs);
return txs;
}
/**
* @inheritDoc
*/
serializeTx(tx: StarknetTx): Promise<string> {
return Promise.resolve(StarknetTransactions.serializeTx(tx));
}
/**
* @inheritDoc
*/
deserializeTx(txData: string): Promise<StarknetTx> {
return Promise.resolve(StarknetTransactions.deserializeTx(txData));
}
/**
* @inheritDoc
*/
serializeSignedTx(signedTx: SignedStarknetTx): Promise<string> {
return Promise.resolve(StarknetTransactions.serializeTx(signedTx));
}
/**
* @inheritDoc
*/
deserializeSignedTx(txData: string): Promise<SignedStarknetTx> {
return Promise.resolve(StarknetTransactions.deserializeTx(txData));
}
/**
* @inheritDoc
*/
getTxId(signedTX: SignedStarknetTx): Promise<string> {
return Promise.resolve(signedTX.txId ?? calculateHash(signedTX));
}
/**
* @inheritDoc
*/
getTxIdStatus(txId: string): Promise<"not_found" | "pending" | "success" | "reverted"> {
return this.Transactions.getTxIdStatus(txId);
}
/**
* @inheritDoc
*/
getTxStatus(tx: string): Promise<"not_found" | "pending" | "success" | "reverted"> {
return this.Transactions.getTxStatus(tx);
}
/**
* @inheritDoc
*/
async getFinalizedBlock(): Promise<{ height: number; blockHash: string }> {
const block = await this.Blocks.getBlock("l1_accepted");
return {
height: block.block_number as number,
blockHash: block.block_hash as string
}
}
/**
* @inheritDoc
*/
txsTransfer(signer: string, token: string, amount: bigint, dstAddress: string, feeRate?: string): Promise<StarknetTx[]> {
return this.Tokens.txsTransfer(signer, token, amount, dstAddress, feeRate);
}
/**
* @inheritDoc
*/
async transfer(
signer: StarknetSigner,
token: string,
amount: bigint,
dstAddress: string,
txOptions?: TransactionConfirmationOptions
): Promise<string> {
const txs = await this.Tokens.txsTransfer(signer.getAddress(), token, amount, dstAddress, txOptions?.feeRate);
const [txId] = await this.Transactions.sendAndConfirm(signer, txs, txOptions?.waitForConfirmation, txOptions?.abortSignal, false);
return txId;
}
/**
* @inheritDoc
*/
wrapSigner(signer: Account): Promise<StarknetSigner> {
if((signer as any).walletProvider!=null) {
return Promise.resolve(new StarknetBrowserSigner(signer));
} else {
return Promise.resolve(new StarknetSigner(signer));
}
}
async verifyNetwork(bitcoinNetwork: BitcoinNetwork): Promise<void> {
if(this.bitcoinNetwork!=null && bitcoinNetwork!==this.bitcoinNetwork)
throw new Error(`Network mismatch, the chain interface was not setup for ${BitcoinNetwork[bitcoinNetwork]}, chain interface network: ${BitcoinNetwork[this.bitcoinNetwork]}`);
const chainId = await this.provider.getChainId();
if(chainId!==constants.StarknetChainId.SN_MAIN && chainId!==constants.StarknetChainId.SN_SEPOLIA) {
logger.warn(`verifyNetwork(): Using non-standard chainId ${chainId}, skipping network verfication!`);
return;
}
if(this.starknetChainId!==chainId)
throw new Error(`Network mismatch, the underlying RPC provider isn't using the correct chainId, expected: ${this.starknetChainId}, provider returned: ${chainId}`);
}
}