UNPKG

@atomiqlabs/chain-starknet

Version:
760 lines (690 loc) 35.3 kB
import {StarknetModule} from "../StarknetModule"; import { Call, DeployAccountContractPayload, DeployAccountContractTransaction, Invocation, InvocationsSignerDetails, BigNumberish, ETransactionStatus, ETransactionExecutionStatus, BlockTag, TransactionFinalityStatus, CallData, Calldata, ResourceBounds, ResourceBoundsBN, RawArgs } from "starknet"; import {StarknetSigner} from "../../wallet/StarknetSigner"; import { calculateHash, deserializeResourceBounds, deserializeSignature, NoBigInt, ReplaceBigInt, serializeResourceBounds, serializeSignature, timeoutPromise, toHex } from "../../../utils/Utils"; import {TransactionRevertedError} from "@atomiqlabs/base"; export type StarknetTxBase = { details: InvocationsSignerDetails & {maxFee?: BigNumberish}, txId?: string }; /** * "INVOKE" type of transaction, used to call smart contracts on Starknet * * @category Chain Interface */ export type StarknetTxInvoke = StarknetTxBase & { type: "INVOKE", tx: Array<Call>, signed?: Invocation }; /** * Type-guard for the "INVOKE" type of transaction, used to call smart contracts on Starknet * * @category Chain Interface */ export function isStarknetTxInvoke(obj: any): obj is StarknetTxInvoke { return typeof(obj)==="object" && typeof(obj.details)==="object" && (obj.txId==null || typeof(obj.txId)==="string") && obj.type==="INVOKE" && Array.isArray(obj.tx) && (obj.signed==null || typeof(obj.signed)==="object"); } /** * "DEPLOY_ACCOUNT" type of transaction, used as a first transaction that the account does to deploy its smart * account contract on the Starknet * * @category Chain Interface */ export type StarknetTxDeployAccount = StarknetTxBase & { type: "DEPLOY_ACCOUNT", tx: DeployAccountContractPayload, signed?: DeployAccountContractTransaction }; /** * Type-guard for the "DEPLOY_ACCOUNT" type of transaction, used as a first transaction that the account does * to deploy its smart account contract on the Starknet * * @category Chain Interface */ export function isStarknetTxDeployAccount(obj: any): obj is StarknetTxDeployAccount { return typeof(obj)==="object" && typeof(obj.details)==="object" && (obj.txId==null || typeof(obj.txId)==="string") && obj.type==="DEPLOY_ACCOUNT" && typeof(obj.tx)==="object" && (obj.signed==null || typeof(obj.signed)==="object"); } /** * Represents a Starknet transactions, which can either be an "INVOKE" or "DEPLOY_ACCOUNT" type, use the * {@link isStarknetTxInvoke} & {@link isStarknetTxDeployAccount} to narrow down the type. * * @category Chain Interface */ export type StarknetTx = StarknetTxInvoke | StarknetTxDeployAccount; /** * Represents a signed Starknet transactions, which can either be an "INVOKE" or "DEPLOY_ACCOUNT" type, use the * {@link isStarknetTxInvoke} & {@link isStarknetTxDeployAccount} to narrow down the type. * * @remarks For Starknet this is just an alias for {@link StarknetTx} * * @category Chain Interface */ export type SignedStarknetTx = StarknetTx; export type StarknetTraceCall = { calldata: string[], contract_address: string, entry_point_selector: string, calls: StarknetTraceCall[] }; const MAX_UNCONFIRMED_TXS = 25; export class StarknetTransactions extends StarknetModule { private readonly latestConfirmedNonces: {[address: string]: bigint} = {}; private readonly latestPendingNonces: {[address: string]: bigint} = {}; private readonly latestSignedNonces: {[address: string]: bigint} = {}; readonly _cbksBeforeTxReplace: ((oldTx: string, oldTxId: string, newTx: string, newTxId: string) => Promise<void>)[] = []; private readonly cbksBeforeTxSigned: ((tx: StarknetTx) => Promise<void>)[] = []; readonly _knownTxSet: Set<string> = new Set(); sendTransaction(tx: StarknetTx): Promise<string> { if(tx.signed==null) throw new Error("Cannot send unsigned transaction! signed field missing!"); switch(tx.type) { case "INVOKE": return this.provider.channel.invoke(tx.signed, tx.details).then(res => res.transaction_hash); case "DEPLOY_ACCOUNT": return this.provider.channel.deployAccount(tx.signed, tx.details).then((res: any) => res.transaction_hash); default: throw new Error("Unsupported tx type!"); } } /** * Returns the nonce of the account or 0, if the account is not deployed yet * * @param address * @param blockTag */ async getNonce(address: string, blockTag: BlockTag = BlockTag.PRE_CONFIRMED): Promise<bigint> { try { return BigInt(await this.provider.getNonceForAddress(address, blockTag)); } catch (e: any) { if( e.baseError?.code === 20 || (e.message!=null && e.message.includes("20: Contract not found")) ) { return BigInt(0); } throw e; } } private async confirmTransactionWs(txId: string, abortSignal?: AbortSignal): Promise<{ txId: string, status: "reverted" | "success" }> { if(this.root.wsChannel==null) throw new Error("Underlying provider doesn't have a WS channel!"); const subscription = await this.root.wsChannel.subscribeTransactionStatus({ transactionHash: txId }); const endSubscription = async () => { if(this.root.wsChannel!.isConnected() && await subscription.unsubscribe()) return; this.root.wsChannel!.removeSubscription(subscription.id); } if(abortSignal!=null && abortSignal.aborted) { await endSubscription(); abortSignal.throwIfAborted(); } const status = await new Promise<"reverted" | "success">((resolve, reject) => { if(abortSignal!=null) abortSignal.onabort = () => { endSubscription().catch(err => this.logger.error("confirmTransactionWs(): End subscription error: ", err)); reject(abortSignal.reason); }; subscription.on((data) => { if(data.status.finality_status!==ETransactionStatus.ACCEPTED_ON_L2 && data.status.finality_status!==ETransactionStatus.ACCEPTED_ON_L1) return; //No pre-confs resolve(data.status.execution_status===ETransactionExecutionStatus.SUCCEEDED ? "success" : "reverted"); }); }); await endSubscription(); this.logger.debug(`confirmTransactionWs(): Transaction ${txId} confirmed, transaction status: ${status}`); return { txId, status }; } private async confirmTransactionPolling(walletAddress: string, nonce: bigint, checkTxns: Set<string>, abortSignal?: AbortSignal): Promise<{ txId?: string, status: "rejected" | "reverted" | "success" }> { let state: "rejected" | "reverted" | "success" | "pending" = "pending"; let confirmedTxId: string | undefined; while(state==="pending") { await timeoutPromise(3000, abortSignal); const latestConfirmedNonce = this.latestConfirmedNonces[toHex(walletAddress)]; const snapshot = [...checkTxns]; //Iterate over a snapshot const totalTxnCount = snapshot.length; let rejectedTxns = 0; let notFoundTxns = 0; for(let txId of snapshot) { let _state = await this._getTxIdStatus(txId); if(_state==="not_found") notFoundTxns++; if(_state==="rejected") rejectedTxns++; if(_state==="reverted" || _state==="success") { confirmedTxId = txId; state = _state; break; } } if(rejectedTxns===totalTxnCount) { //All rejected state = "rejected"; break; } if(notFoundTxns===totalTxnCount) { //All not found, check the latest account nonce if(latestConfirmedNonce!=null && latestConfirmedNonce>nonce) { //Confirmed nonce is already higher than the TX nonce, meaning the TX got replaced throw new Error("Transaction failed - replaced!"); } this.logger.warn("confirmTransaction(): All transactions not found, fetching the latest account nonce..."); const _latestConfirmedNonce = this.latestConfirmedNonces[toHex(walletAddress)]; const currentLatestNonce = await this.getNonce(walletAddress, BlockTag.LATEST); if(_latestConfirmedNonce==null || _latestConfirmedNonce < currentLatestNonce) { this.latestConfirmedNonces[toHex(walletAddress)] = currentLatestNonce; } } } if(state!=="rejected") this.logger.debug(`confirmTransactionPolling(): Transaction ${confirmedTxId} confirmed, transaction status: ${state}`); return { txId: confirmedTxId, status: state } } /** * Waits for transaction confirmation using WS subscription and occasional HTTP polling, also re-sends * the transaction at regular interval * * @param tx starknet transaction to wait for confirmation for & keep re-sending until it confirms * @param abortSignal signal to abort waiting for tx confirmation * @private */ private async confirmTransaction(tx: StarknetTx, abortSignal?: AbortSignal): Promise<string> { if(tx.txId==null) throw new Error("txId is null!"); const abortController = new AbortController(); if(abortSignal!=null) abortSignal.onabort = () => abortController.abort(abortSignal.reason); let txReplaceListener: ((oldTx: string, oldTxId: string, newTx: string, newTxId: string) => Promise<void>) | undefined = undefined; let result: { txId?: string, status: "rejected" | "reverted" | "success" }; try { result = await new Promise<{ txId?: string, status: "rejected" | "reverted" | "success" }>((resolve, reject) => { const checkTxns: Set<string> = new Set([tx.txId!]); txReplaceListener = (oldTx: string, oldTxId: string, newTx: string, newTxId: string) => { if(checkTxns.has(oldTxId)) checkTxns.add(newTxId); //TODO: Enable this once WS subscriptions finally work (also unsubscribe should work!!!!) // if(this.root.wsChannel!=null) this.confirmTransactionWs(newTxId, abortController.signal) // .then(resolve) // .catch(reject); return Promise.resolve(); }; this.onBeforeTxReplace(txReplaceListener); this.confirmTransactionPolling(tx.details.walletAddress, BigInt(tx.details.nonce), checkTxns, abortController.signal) .then(resolve) .catch(reject); //TODO: Enable this once WS subscriptions finally work (also unsubscribe should work!!!!) // if(this.root.wsChannel!=null) this.confirmTransactionWs(tx.txId!, abortController.signal) // .then(resolve) // .catch(reject); }); if(txReplaceListener!=null) this.offBeforeTxReplace(txReplaceListener); abortController.abort(); } catch (e) { if(txReplaceListener!=null) this.offBeforeTxReplace(txReplaceListener); abortController.abort(e); throw e; } if(result.status==="rejected") throw new Error("Transaction rejected!"); const nextAccountNonce = BigInt(tx.details.nonce) + 1n; const currentConfirmedNonce = this.latestConfirmedNonces[toHex(tx.details.walletAddress)]; if(currentConfirmedNonce==null || nextAccountNonce > currentConfirmedNonce) { this.latestConfirmedNonces[toHex(tx.details.walletAddress)] = nextAccountNonce; } if(result.status==="reverted") throw new TransactionRevertedError("Transaction reverted!"); return result.txId!; } /** * Prepares starknet transactions, checks if the account is deployed, assigns nonces if needed * & calls beforeTxSigned callback (only if signer is passed!) * * @param signer * @param txs */ public async prepareTransactions(txs: (StarknetTx & {addedInPrepare?: boolean})[], signer?: StarknetSigner): Promise<void> { if(txs.length===0) return; const signerAddress = signer?.getAddress() ?? txs[0].details.walletAddress; if(signerAddress==null) throw new Error("Cannot get tx sender address!"); let nonce: bigint = await this.getNonce(signerAddress); const latestPendingNonce = this.latestPendingNonces[toHex(signerAddress)]; if(latestPendingNonce!=null && latestPendingNonce > nonce) { this.logger.debug("prepareTransactions(): Using 'pending' nonce from local cache!"); nonce = latestPendingNonce; } //Add deploy account tx if(nonce===0n) { if(signer!=null) { const deployPayload = await signer.getDeployPayload(); if(deployPayload!=null) { const tx: (StarknetTx & {addedInPrepare?: boolean}) = await this.root.Accounts.getAccountDeployTransaction(deployPayload); tx.addedInPrepare = true; txs.unshift(tx); } } else { // Use a 0x0 class hash to indicate that deployment is needed by external signer const tx: (StarknetTx & {addedInPrepare?: boolean}) = await this.root.Accounts.getAccountDeployTransaction({ classHash: "0x0000000000000000000000000000000000000000000000000000000000000000", contractAddress: signerAddress }); tx.addedInPrepare = true; txs.unshift(tx); } } if(signer==null || !signer.isManagingNoncesInternally) { if(nonce===0n) { //Just increment the nonce by one and hope the wallet is smart enough to deploy account first nonce = 1n; } for(let i=0;i<txs.length;i++) { const tx = txs[i]; if(tx.details.nonce!=null) nonce = BigInt(tx.details.nonce); //Take the nonce from last tx if(nonce==null) nonce = BigInt(await this.root.provider.getNonceForAddress(signerAddress)); //Fetch the nonce if(tx.details.nonce==null) tx.details.nonce = nonce; this.logger.debug("prepareTransactions(): transaction prepared ("+(i+1)+"/"+txs.length+"), nonce: "+tx.details.nonce); nonce += BigInt(1); } } if(signer!=null) for(let tx of txs) { for(let callback of this.cbksBeforeTxSigned) { await callback(tx); } } } /** * Sends out a signed transaction to the RPC * * @param tx Starknet tx to send * @param onBeforePublish a callback called before every transaction is published * @private */ private async sendSignedTransaction( tx: StarknetTx, onBeforePublish?: (txId: string, rawTx: string) => Promise<void> ): Promise<string> { if(tx.txId==null) throw new Error("Expecting signed tx with txId field populated!"); if(onBeforePublish!=null) await onBeforePublish(tx.txId, StarknetTransactions.serializeTx(tx)); this.logger.debug("sendSignedTransaction(): sending transaction: ", tx.txId); const txResult: string = await this.sendTransaction(tx); if(tx.txId!==txResult) this.logger.warn("sendSignedTransaction(): sent tx hash not matching the precomputed hash!"); this.logger.info("sendSignedTransaction(): tx sent, expected txHash: "+tx.txId+", txHash: "+txResult); return txResult; } /** * Prepares, signs , sends (in parallel or sequentially) & optionally waits for confirmation * of a batch of starknet transactions * * @param signer * @param _txs transactions to send * @param waitForConfirmation whether to wait for transaction confirmations (this also makes sure the transactions * are re-sent at regular intervals) * @param abortSignal abort signal to abort waiting for transaction confirmations * @param parallel whether the send all the transaction at once in parallel or sequentially (such that transactions * are executed in order) * @param onBeforePublish a callback called before every transaction is published */ public async sendAndConfirm(signer: StarknetSigner, _txs: StarknetTx[], waitForConfirmation?: boolean, abortSignal?: AbortSignal, parallel?: boolean, onBeforePublish?: (txId: string, rawTx: string) => Promise<void>): Promise<string[]> { const txs: (StarknetTx & {addedInPrepare?: boolean})[] = _txs; await this.prepareTransactions(txs, signer); const signedTxs: (StarknetTx & {addedInPrepare?: boolean})[] = []; //Don't separate the signing process from the sending when using browser-based wallet if(signer.signTransaction!=null) for(let i=0;i<txs.length;i++) { const tx = txs[i]; const signedTx: (StarknetTx & {addedInPrepare?: boolean}) = await signer.signTransaction(tx); calculateHash(signedTx); signedTx.addedInPrepare = tx.addedInPrepare; signedTxs.push(signedTx); this.logger.debug("sendAndConfirm(): transaction signed ("+(i+1)+"/"+txs.length+"): "+signedTx.txId); const nextAccountNonce = BigInt(signedTx.details.nonce) + 1n; const currentSignedNonce = this.latestSignedNonces[toHex(signedTx.details.walletAddress)]; if(currentSignedNonce==null || nextAccountNonce > currentSignedNonce) { this.latestSignedNonces[toHex(signedTx.details.walletAddress)] = nextAccountNonce; } } this.logger.debug("sendAndConfirm(): sending transactions, count: "+txs.length+ " waitForConfirmation: "+waitForConfirmation+" parallel: "+parallel); const txIds: string[] = []; if(parallel) { let promises: Promise<string>[] = []; for(let i=0;i<txs.length;i++) { let tx: (StarknetTx & {addedInPrepare?: boolean}); if(signer.signTransaction==null) { const txId = await signer.sendTransaction(txs[i], txs[i].addedInPrepare ? undefined : onBeforePublish); tx = txs[i]; tx.txId = txId; } else { const signedTx = signedTxs[i]; await this.sendSignedTransaction(signedTx, signedTx.addedInPrepare ? undefined : onBeforePublish); tx = signedTx; } if(tx.details.nonce!=null) { const nextAccountNonce = BigInt(tx.details.nonce) + 1n; const currentPendingNonce = this.latestPendingNonces[toHex(tx.details.walletAddress)]; if(currentPendingNonce==null || nextAccountNonce > currentPendingNonce) { this.latestPendingNonces[toHex(tx.details.walletAddress)] = nextAccountNonce; } } if(!tx.addedInPrepare) { promises.push(this.confirmTransaction(tx, abortSignal)); if(!waitForConfirmation) txIds.push(tx.txId!); } this.logger.debug("sendAndConfirm(): transaction sent ("+(i+1)+"/"+txs.length+"): "+tx.txId); if(promises.length >= MAX_UNCONFIRMED_TXS) { if(waitForConfirmation) txIds.push(...await Promise.all(promises)); promises = []; } } if(waitForConfirmation && promises.length>0) { txIds.push(...await Promise.all(promises)); } } else { for(let i=0;i<txs.length;i++) { let tx: (StarknetTx & {addedInPrepare?: boolean}); if(signer.signTransaction==null) { const txId = await signer.sendTransaction(txs[i], txs[i].addedInPrepare ? undefined : onBeforePublish); tx = txs[i]; tx.txId = txId; } else { const signedTx = signedTxs[i]; await this.sendSignedTransaction(signedTx, signedTx.addedInPrepare ? undefined : onBeforePublish); tx = signedTx; } if(tx.details.nonce!=null) { const nextAccountNonce = BigInt(tx.details.nonce) + 1n; const currentPendingNonce = this.latestPendingNonces[toHex(tx.details.walletAddress)]; if(currentPendingNonce==null || nextAccountNonce > currentPendingNonce) { this.latestPendingNonces[toHex(tx.details.walletAddress)] = nextAccountNonce; } } const confirmPromise = this.confirmTransaction(tx, abortSignal); this.logger.debug("sendAndConfirm(): transaction sent ("+(i+1)+"/"+txs.length+"): "+tx.txId); //Don't await the last promise when !waitForConfirmation let txHash = tx.txId!; if(i<txs.length-1 || waitForConfirmation) txHash = await confirmPromise; if(!tx.addedInPrepare) txIds.push(txHash); } } this.logger.info("sendAndConfirm(): sent transactions, count: "+txs.length+ " waitForConfirmation: "+waitForConfirmation+" parallel: "+parallel); return txIds; } public async sendSignedAndConfirm( signedTxs: SignedStarknetTx[], waitForConfirmation?: boolean, abortSignal?: AbortSignal, parallel?: boolean, onBeforePublish?: (txId: string, rawTx: string) => Promise<void> ): Promise<string[]> { signedTxs.forEach(tx => { if(tx.signed==null) throw new Error("Transactions have to be signed!"); calculateHash(tx); }); this.logger.debug("sendSignedAndConfirm(): sending transactions, count: "+signedTxs.length+ " waitForConfirmation: "+waitForConfirmation+" parallel: "+parallel); const txIds: string[] = []; if(parallel) { let promises: Promise<string>[] = []; for(let i=0;i<signedTxs.length;i++) { const signedTx = signedTxs[i]; await this.sendSignedTransaction(signedTx, onBeforePublish); if(signedTx.details.nonce!=null) { const nextAccountNonce = BigInt(signedTx.details.nonce) + 1n; const currentPendingNonce = this.latestPendingNonces[toHex(signedTx.details.walletAddress)]; if(currentPendingNonce==null || nextAccountNonce > currentPendingNonce) { this.latestPendingNonces[toHex(signedTx.details.walletAddress)] = nextAccountNonce; } } promises.push(this.confirmTransaction(signedTx, abortSignal)); if(!waitForConfirmation) txIds.push(signedTx.txId!); this.logger.debug("sendSignedAndConfirm(): transaction sent ("+(i+1)+"/"+signedTxs.length+"): "+signedTx.txId); if(promises.length >= MAX_UNCONFIRMED_TXS) { if(waitForConfirmation) txIds.push(...await Promise.all(promises)); promises = []; } } if(waitForConfirmation && promises.length>0) { txIds.push(...await Promise.all(promises)); } } else { for(let i=0;i<signedTxs.length;i++) { const signedTx = signedTxs[i]; await this.sendSignedTransaction(signedTx, onBeforePublish); if(signedTx.details.nonce!=null) { const nextAccountNonce = BigInt(signedTx.details.nonce) + 1n; const currentPendingNonce = this.latestPendingNonces[toHex(signedTx.details.walletAddress)]; if(currentPendingNonce==null || nextAccountNonce > currentPendingNonce) { this.latestPendingNonces[toHex(signedTx.details.walletAddress)] = nextAccountNonce; } } const confirmPromise = this.confirmTransaction(signedTx, abortSignal); this.logger.debug("sendSignedAndConfirm(): transaction sent ("+(i+1)+"/"+signedTxs.length+"): "+signedTx.txId); //Don't await the last promise when !waitForConfirmation let txHash = signedTx.txId!; if(i<signedTxs.length-1 || waitForConfirmation) txHash = await confirmPromise; txIds.push(txHash); } } this.logger.info("sendSignedAndConfirm(): sent transactions, count: "+signedTxs.length+ " waitForConfirmation: "+waitForConfirmation+" parallel: "+parallel); return txIds; } /** * Serializes the starknet transaction, saves the transaction, signers & last valid blockheight * * @param tx */ public static serializeTx(tx: StarknetTx): string { const details: ReplaceBigInt<InvocationsSignerDetails & {maxFee?: BigNumberish}> = { ...tx.details, nonce: toHex(tx.details.nonce), resourceBounds: serializeResourceBounds(tx.details.resourceBounds), tip: toHex(tx.details.tip), paymasterData: tx.details.paymasterData.map(val => toHex(val)), accountDeploymentData: tx.details.accountDeploymentData.map(val => toHex(val)), maxFee: tx.details.maxFee==null ? undefined : toHex(tx.details.maxFee) }; if(isStarknetTxInvoke(tx)) { const calls: (ReplaceBigInt<Call> & {calldata: Calldata})[] = tx.tx.map(call => ({ ...call, calldata: call.calldata==null ? [] : CallData.compile(call.calldata), })); const signed: (ReplaceBigInt<Invocation> & {calldata: Calldata, resourceBounds?: ResourceBounds}) | undefined = tx.signed==null ? undefined : { ...tx.signed, resourceBounds: (tx.signed as any).resourceBounds==null ? undefined : serializeResourceBounds((tx.signed as any).resourceBounds), calldata: tx.signed.calldata==null ? [] : CallData.compile(tx.signed.calldata), signature: serializeSignature(tx.signed.signature) }; return JSON.stringify({ type: tx.type, tx: calls, details, signed, txId: tx.txId }); } else if(isStarknetTxDeployAccount(tx)) { const deployPaylod: ReplaceBigInt<DeployAccountContractPayload> & {constructorCalldata: Calldata} = { ...tx.tx, constructorCalldata: tx.tx.constructorCalldata==null ? [] : CallData.compile(tx.tx.constructorCalldata), addressSalt: toHex(tx.tx.addressSalt) ?? undefined }; const signed: (ReplaceBigInt<DeployAccountContractTransaction> & {constructorCalldata: Calldata, resourceBounds?: ResourceBounds}) | undefined = tx.signed==null ? undefined : { ...tx.signed, resourceBounds: (tx.signed as any).resourceBounds==null ? undefined : serializeResourceBounds((tx.signed as any).resourceBounds), constructorCalldata: tx.tx.constructorCalldata==null ? [] : CallData.compile(tx.tx.constructorCalldata), addressSalt: toHex(tx.tx.addressSalt) ?? undefined, signature: serializeSignature(tx.signed.signature) }; return JSON.stringify({ type: tx.type, tx: deployPaylod, details, signed, txId: tx.txId }); } else throw new Error(`Unknown transaction type: ${(tx as any).type}`); } /** * Deserializes saved starknet transaction, extracting the transaction, signers & last valid blockheight * * @param txData */ public static deserializeTx(txData: string): StarknetTx { const _serializedTx = JSON.parse(txData, (key, value) => { //For backwards compatibility if(typeof(value)==="object" && value._type==="bigint") return value._value; return value; }); const serializedDetails: ReplaceBigInt<InvocationsSignerDetails & {maxFee?: BigNumberish}> = _serializedTx.details; const details: InvocationsSignerDetails & {maxFee?: BigNumberish} = { ...serializedDetails, resourceBounds: deserializeResourceBounds(serializedDetails.resourceBounds) }; if(_serializedTx.type==="INVOKE") { const serializedSignedTx: ReplaceBigInt<Invocation> | undefined = _serializedTx.signed; const signed: Invocation | undefined = serializedSignedTx==null ? undefined : { ...serializedSignedTx, signature: deserializeSignature(serializedSignedTx.signature) }; const serializedCalls: ReplaceBigInt<Call>[] = _serializedTx.tx; const calls: Call[] = serializedCalls; return { type: "INVOKE", tx: calls, details, signed, txId: _serializedTx.txId } } else if(_serializedTx.type==="DEPLOY_ACCOUNT") { const serializedSignedTx: ReplaceBigInt<DeployAccountContractTransaction> | undefined = _serializedTx.signed; const signed: DeployAccountContractTransaction | undefined = serializedSignedTx==null ? undefined : { ...serializedSignedTx, signature: deserializeSignature(serializedSignedTx.signature) }; const serializedPayload: ReplaceBigInt<DeployAccountContractPayload> = _serializedTx.tx; const payload: DeployAccountContractPayload = serializedPayload; return { type: "DEPLOY_ACCOUNT", tx: payload, details, signed, txId: _serializedTx.txId } } else throw new Error(`Unknown transaction type: ${_serializedTx.type}`); } /** * Gets the status of the raw starknet transaction * * @param tx */ public async getTxStatus(tx: string): Promise<"pending" | "success" | "not_found" | "reverted"> { const parsedTx: StarknetTx = StarknetTransactions.deserializeTx(tx); if(parsedTx.txId==null) throw new Error("Expected signed transaction with txId field populated!"); return await this.getTxIdStatus(parsedTx.txId); } /** * Gets the status of the starknet transaction with a specific txId * * @param txId */ public async _getTxIdStatus(txId: string): Promise<"pending" | "success" | "not_found" | "reverted" | "rejected"> { const status = await this.provider.getTransactionStatus(txId).catch(e => { if( e.baseError?.code===29 || (e.message!=null && e.message.includes("29: Transaction hash not found")) ) return null; throw e; }); if(status==null) return this._knownTxSet.has(txId) ? "pending" : "not_found"; // REJECTED status was removed in starknet.js v9 - transactions are now either accepted or reverted if(status.finality_status!==ETransactionStatus.ACCEPTED_ON_L2 && status.finality_status!==ETransactionStatus.ACCEPTED_ON_L1) return "pending"; if(status.execution_status===ETransactionExecutionStatus.SUCCEEDED){ return "success"; } return "reverted"; } /** * Gets the status of the starknet transaction with a specific txId * * @param txId */ public async getTxIdStatus(txId: string): Promise<"pending" | "success" | "not_found" | "reverted"> { const status = await this._getTxIdStatus(txId); if(status==="rejected") return "reverted"; return status; } async traceTransaction(txId: string, blockHash?: string): Promise<StarknetTraceCall | null> { let trace: any; try { trace = await this.provider.getTransactionTrace(txId); } catch (e) { this.logger.warn("getSwapDataGetter(): getter: starknet_traceTransaction not supported by the RPC: ", e); if(blockHash==null) throw e; const blockTraces: any[] = await this.provider.getBlockTransactionsTraces(blockHash); const foundTrace = blockTraces.find(val => toHex(val.transaction_hash)===toHex(txId)); if(foundTrace==null) throw new Error(`Cannot find ${txId} in the block traces, block: ${blockHash}`); trace = foundTrace.trace_root; } if(trace==null) return null; if(trace.execute_invocation.revert_reason!=null) return null; return trace.execute_invocation; } onBeforeTxReplace(callback: (oldTx: string, oldTxId: string, newTx: string, newTxId: string) => Promise<void>): void { this._cbksBeforeTxReplace.push(callback); } offBeforeTxReplace(callback: (oldTx: string, oldTxId: string, newTx: string, newTxId: string) => Promise<void>): boolean { const index = this._cbksBeforeTxReplace.indexOf(callback); if(index===-1) return false; this._cbksBeforeTxReplace.splice(index, 1); return true; } public onBeforeTxSigned(callback: (tx: StarknetTx) => Promise<void>): void { this.cbksBeforeTxSigned.push(callback); } public offBeforeTxSigned(callback: (tx: StarknetTx) => Promise<void>): boolean { const index = this.cbksBeforeTxSigned.indexOf(callback); if(index===-1) return false; this.cbksBeforeTxSigned.splice(index, 1); return true; } }