UNPKG

@atomiqlabs/chain-starknet

Version:
335 lines (294 loc) 11 kB
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}`); } }