@atomiqlabs/chain-evm 1.0.0-dev.99 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chains/botanix/BotanixInitializer.d.ts +1 -1
- package/dist/chains/botanix/BotanixInitializer.js +1 -0
- package/dist/chains/citrea/CitreaInitializer.d.ts +1 -1
- package/dist/chains/citrea/CitreaInitializer.js +1 -0
- package/dist/evm/chain/EVMChainInterface.d.ts +5 -0
- package/dist/evm/chain/EVMChainInterface.js +9 -1
- package/dist/evm/chain/modules/EVMBlocks.d.ts +7 -0
- package/dist/evm/chain/modules/EVMBlocks.js +16 -7
- package/dist/evm/chain/modules/EVMEvents.js +2 -1
- package/dist/evm/chain/modules/EVMTransactions.js +2 -1
- package/dist/evm/contract/modules/EVMContractEvents.d.ts +2 -1
- package/dist/evm/contract/modules/EVMContractEvents.js +3 -2
- package/dist/evm/providers/ReconnectingWebSocketProvider.js +2 -2
- package/dist/evm/spv_swap/EVMSpvVaultContract.d.ts +6 -3
- package/dist/evm/spv_swap/EVMSpvVaultContract.js +85 -34
- package/dist/evm/wallet/EVMPersistentSigner.d.ts +1 -0
- package/dist/evm/wallet/EVMPersistentSigner.js +29 -4
- package/package.json +2 -2
- package/src/chains/botanix/BotanixInitializer.ts +2 -1
- package/src/chains/citrea/CitreaInitializer.ts +2 -1
- package/src/evm/chain/EVMChainInterface.ts +10 -0
- package/src/evm/chain/modules/EVMBlocks.ts +20 -9
- package/src/evm/chain/modules/EVMEvents.ts +1 -0
- package/src/evm/chain/modules/EVMTransactions.ts +2 -1
- package/src/evm/contract/modules/EVMContractEvents.ts +3 -1
- package/src/evm/providers/ReconnectingWebSocketProvider.ts +2 -2
- package/src/evm/spv_swap/EVMSpvVaultContract.ts +100 -44
- package/src/evm/wallet/EVMPersistentSigner.ts +30 -4
|
@@ -23,7 +23,7 @@ export type BotanixOptions = {
|
|
|
23
23
|
};
|
|
24
24
|
};
|
|
25
25
|
fees?: EVMFees;
|
|
26
|
-
evmConfig?: Omit<EVMConfiguration, "safeBlockTag">;
|
|
26
|
+
evmConfig?: Omit<EVMConfiguration, "safeBlockTag" | "finalizedBlockTag">;
|
|
27
27
|
};
|
|
28
28
|
export declare function initializeBotanix(options: BotanixOptions, bitcoinRpc: BitcoinRpc<any>, network: BitcoinNetwork): ChainData<BotanixChainType>;
|
|
29
29
|
export type BotanixInitializerType = ChainInitializer<BotanixOptions, BotanixChainType, BotanixAssetsType>;
|
|
@@ -84,6 +84,7 @@ function initializeBotanix(options, bitcoinRpc, network) {
|
|
|
84
84
|
const Fees = options.fees ?? new EVMFees_1.EVMFees(provider, 2n * 1000000000n, 1000000n);
|
|
85
85
|
const chainInterface = new EVMChainInterface_1.EVMChainInterface("BOTANIX", chainId, provider, {
|
|
86
86
|
safeBlockTag: "finalized",
|
|
87
|
+
finalizedBlockTag: "finalized",
|
|
87
88
|
maxLogsBlockRange: options?.evmConfig?.maxLogsBlockRange ?? 950,
|
|
88
89
|
maxLogTopics: options?.evmConfig?.maxLogTopics ?? 64,
|
|
89
90
|
maxParallelLogRequests: options?.evmConfig?.maxParallelLogRequests ?? 5,
|
|
@@ -23,7 +23,7 @@ export type CitreaOptions = {
|
|
|
23
23
|
};
|
|
24
24
|
};
|
|
25
25
|
fees?: CitreaFees;
|
|
26
|
-
evmConfig?: Omit<EVMConfiguration, "safeBlockTag">;
|
|
26
|
+
evmConfig?: Omit<EVMConfiguration, "safeBlockTag" | "finalizedBlockTag">;
|
|
27
27
|
};
|
|
28
28
|
export declare function initializeCitrea(options: CitreaOptions, bitcoinRpc: BitcoinRpc<any>, network: BitcoinNetwork): ChainData<CitreaChainType>;
|
|
29
29
|
export type CitreaInitializerType = ChainInitializer<CitreaOptions, CitreaChainType, CitreaAssetsType>;
|
|
@@ -90,6 +90,7 @@ function initializeCitrea(options, bitcoinRpc, network) {
|
|
|
90
90
|
const Fees = options.fees ?? new CitreaFees_1.CitreaFees(provider, 2n * 1000000000n, 1000000n);
|
|
91
91
|
const chainInterface = new EVMChainInterface_1.EVMChainInterface("CITREA", chainId, provider, {
|
|
92
92
|
safeBlockTag: "latest",
|
|
93
|
+
finalizedBlockTag: "safe",
|
|
93
94
|
maxLogsBlockRange: options?.evmConfig?.maxLogsBlockRange ?? 950,
|
|
94
95
|
maxLogTopics: options?.evmConfig?.maxLogTopics ?? 64,
|
|
95
96
|
maxParallelLogRequests: options?.evmConfig?.maxParallelLogRequests ?? 5,
|
|
@@ -15,6 +15,7 @@ export type EVMRetryPolicy = {
|
|
|
15
15
|
};
|
|
16
16
|
export type EVMConfiguration = {
|
|
17
17
|
safeBlockTag: EVMBlockTag;
|
|
18
|
+
finalizedBlockTag: EVMBlockTag;
|
|
18
19
|
maxLogsBlockRange: number;
|
|
19
20
|
maxParallelLogRequests: number;
|
|
20
21
|
maxParallelCalls: number;
|
|
@@ -50,6 +51,10 @@ export declare class EVMChainInterface<ChainId extends string = string> implemen
|
|
|
50
51
|
deserializeTx(txData: string): Promise<Transaction>;
|
|
51
52
|
getTxIdStatus(txId: string): Promise<"not_found" | "pending" | "success" | "reverted">;
|
|
52
53
|
getTxStatus(tx: string): Promise<"not_found" | "pending" | "success" | "reverted">;
|
|
54
|
+
getFinalizedBlock(): Promise<{
|
|
55
|
+
height: number;
|
|
56
|
+
blockHash: string;
|
|
57
|
+
}>;
|
|
53
58
|
txsTransfer(signer: string, token: string, amount: bigint, dstAddress: string, feeRate?: string): Promise<TransactionRequest[]>;
|
|
54
59
|
transfer(signer: EVMSigner, token: string, amount: bigint, dstAddress: string, txOptions?: TransactionConfirmationOptions): Promise<string>;
|
|
55
60
|
wrapSigner(signer: Signer): Promise<EVMSigner>;
|
|
@@ -14,13 +14,14 @@ const EVMSigner_1 = require("../wallet/EVMSigner");
|
|
|
14
14
|
const EVMBrowserSigner_1 = require("../wallet/EVMBrowserSigner");
|
|
15
15
|
class EVMChainInterface {
|
|
16
16
|
constructor(chainId, evmChainId, provider, config, retryPolicy, evmFeeEstimator = new EVMFees_1.EVMFees(provider)) {
|
|
17
|
-
var _a;
|
|
17
|
+
var _a, _b;
|
|
18
18
|
this.chainId = chainId;
|
|
19
19
|
this.evmChainId = evmChainId;
|
|
20
20
|
this.provider = provider;
|
|
21
21
|
this.retryPolicy = retryPolicy;
|
|
22
22
|
this.config = config;
|
|
23
23
|
(_a = this.config).safeBlockTag ?? (_a.safeBlockTag = "safe");
|
|
24
|
+
(_b = this.config).finalizedBlockTag ?? (_b.finalizedBlockTag = "finalized");
|
|
24
25
|
this.logger = (0, Utils_1.getLogger)("EVMChainInterface(" + this.evmChainId + "): ");
|
|
25
26
|
this.Fees = evmFeeEstimator;
|
|
26
27
|
this.Tokens = new EVMTokens_1.EVMTokens(this);
|
|
@@ -81,6 +82,13 @@ class EVMChainInterface {
|
|
|
81
82
|
getTxStatus(tx) {
|
|
82
83
|
return this.Transactions.getTxStatus(tx);
|
|
83
84
|
}
|
|
85
|
+
async getFinalizedBlock() {
|
|
86
|
+
const block = await this.Blocks.getBlock(this.config.finalizedBlockTag);
|
|
87
|
+
return {
|
|
88
|
+
height: block.number,
|
|
89
|
+
blockHash: block.hash
|
|
90
|
+
};
|
|
91
|
+
}
|
|
84
92
|
async txsTransfer(signer, token, amount, dstAddress, feeRate) {
|
|
85
93
|
return [await this.Tokens.Transfer(signer, token, amount, dstAddress, feeRate)];
|
|
86
94
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EVMModule } from "../EVMModule";
|
|
2
|
+
import { Block } from "ethers";
|
|
2
3
|
export type EVMBlockTag = "safe" | "pending" | "latest" | "finalized";
|
|
3
4
|
export declare class EVMBlocks extends EVMModule<any> {
|
|
4
5
|
private BLOCK_CACHE_TIME;
|
|
@@ -16,5 +17,11 @@ export declare class EVMBlocks extends EVMModule<any> {
|
|
|
16
17
|
*
|
|
17
18
|
* @param blockTag
|
|
18
19
|
*/
|
|
20
|
+
getBlock(blockTag: EVMBlockTag | number): Promise<Block>;
|
|
21
|
+
/**
|
|
22
|
+
* Gets the block time for a given blocktag, with caching
|
|
23
|
+
*
|
|
24
|
+
* @param blockTag
|
|
25
|
+
*/
|
|
19
26
|
getBlockTime(blockTag: EVMBlockTag | number): Promise<number>;
|
|
20
27
|
}
|
|
@@ -16,19 +16,19 @@ class EVMBlocks extends EVMModule_1.EVMModule {
|
|
|
16
16
|
*/
|
|
17
17
|
fetchAndSaveBlockTime(blockTag) {
|
|
18
18
|
const blockTagStr = blockTag.toString(10);
|
|
19
|
-
const
|
|
19
|
+
const blockPromise = this.provider.getBlock(blockTag, false);
|
|
20
20
|
const timestamp = Date.now();
|
|
21
21
|
this.blockCache[blockTagStr] = {
|
|
22
|
-
|
|
22
|
+
block: blockPromise,
|
|
23
23
|
timestamp
|
|
24
24
|
};
|
|
25
|
-
|
|
26
|
-
if (this.blockCache[blockTagStr] != null && this.blockCache[blockTagStr].
|
|
25
|
+
blockPromise.catch(e => {
|
|
26
|
+
if (this.blockCache[blockTagStr] != null && this.blockCache[blockTagStr].block === blockPromise)
|
|
27
27
|
delete this.blockCache[blockTagStr];
|
|
28
28
|
throw e;
|
|
29
29
|
});
|
|
30
30
|
return {
|
|
31
|
-
|
|
31
|
+
block: blockPromise,
|
|
32
32
|
timestamp
|
|
33
33
|
};
|
|
34
34
|
}
|
|
@@ -52,13 +52,22 @@ class EVMBlocks extends EVMModule_1.EVMModule {
|
|
|
52
52
|
*
|
|
53
53
|
* @param blockTag
|
|
54
54
|
*/
|
|
55
|
-
|
|
55
|
+
getBlock(blockTag) {
|
|
56
56
|
this.cleanupBlocks();
|
|
57
57
|
let cachedBlockData = this.blockCache[blockTag.toString(10)];
|
|
58
58
|
if (cachedBlockData == null || Date.now() - cachedBlockData.timestamp > this.BLOCK_CACHE_TIME) {
|
|
59
59
|
cachedBlockData = this.fetchAndSaveBlockTime(blockTag);
|
|
60
60
|
}
|
|
61
|
-
return cachedBlockData.
|
|
61
|
+
return cachedBlockData.block;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Gets the block time for a given blocktag, with caching
|
|
65
|
+
*
|
|
66
|
+
* @param blockTag
|
|
67
|
+
*/
|
|
68
|
+
async getBlockTime(blockTag) {
|
|
69
|
+
const block = await this.getBlock(blockTag);
|
|
70
|
+
return block.timestamp;
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
73
|
exports.EVMBlocks = EVMBlocks;
|
|
@@ -22,7 +22,8 @@ class EVMEvents extends EVMModule_1.EVMModule {
|
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
catch (e) {
|
|
25
|
-
if (e.error?.code === -
|
|
25
|
+
if ((e.error?.code === -32602 && e.error?.message?.startsWith("query exceeds max results")) || //Query exceeds max results
|
|
26
|
+
e.error?.code === -32008 || //Response is too big
|
|
26
27
|
e.error?.code === -32005 //Limit exceeded
|
|
27
28
|
) {
|
|
28
29
|
if (startBlock === endBlock)
|
|
@@ -4,6 +4,7 @@ exports.EVMTransactions = void 0;
|
|
|
4
4
|
const EVMModule_1 = require("../EVMModule");
|
|
5
5
|
const ethers_1 = require("ethers");
|
|
6
6
|
const Utils_1 = require("../../../utils/Utils");
|
|
7
|
+
const base_1 = require("@atomiqlabs/base");
|
|
7
8
|
const MAX_UNCONFIRMED_TXNS = 10;
|
|
8
9
|
class EVMTransactions extends EVMModule_1.EVMModule {
|
|
9
10
|
constructor() {
|
|
@@ -68,7 +69,7 @@ class EVMTransactions extends EVMModule_1.EVMModule {
|
|
|
68
69
|
this.latestConfirmedNonces[tx.from] = nextAccountNonce;
|
|
69
70
|
}
|
|
70
71
|
if (state === "reverted")
|
|
71
|
-
throw new
|
|
72
|
+
throw new base_1.TransactionRevertedError("Transaction reverted!");
|
|
72
73
|
return confirmedTxId;
|
|
73
74
|
}
|
|
74
75
|
/**
|
|
@@ -36,7 +36,8 @@ export declare class EVMContractEvents<T extends BaseContract> extends EVMEvents
|
|
|
36
36
|
* @param keys
|
|
37
37
|
* @param processor called for every event, should return a value if the correct event was found, or null
|
|
38
38
|
* if the search should continue
|
|
39
|
+
* @param startHeight
|
|
39
40
|
* @param abortSignal
|
|
40
41
|
*/
|
|
41
|
-
findInContractEventsForward<TResult, TEventName extends keyof T["filters"]>(events: TEventName[], keys: (string | string[])[], processor: (event: TypedEventLog<T["filters"][TEventName]>) => Promise<TResult>, abortSignal?: AbortSignal): Promise<TResult>;
|
|
42
|
+
findInContractEventsForward<TResult, TEventName extends keyof T["filters"]>(events: TEventName[], keys: (string | string[])[], processor: (event: TypedEventLog<T["filters"][TEventName]>) => Promise<TResult>, startHeight?: number, abortSignal?: AbortSignal): Promise<TResult>;
|
|
42
43
|
}
|
|
@@ -59,9 +59,10 @@ class EVMContractEvents extends EVMEvents_1.EVMEvents {
|
|
|
59
59
|
* @param keys
|
|
60
60
|
* @param processor called for every event, should return a value if the correct event was found, or null
|
|
61
61
|
* if the search should continue
|
|
62
|
+
* @param startHeight
|
|
62
63
|
* @param abortSignal
|
|
63
64
|
*/
|
|
64
|
-
async findInContractEventsForward(events, keys, processor, abortSignal) {
|
|
65
|
+
async findInContractEventsForward(events, keys, processor, startHeight, abortSignal) {
|
|
65
66
|
return this.findInEventsForward(await this.baseContract.getAddress(), this.toFilter(events, keys), async (events) => {
|
|
66
67
|
const parsedEvents = this.toTypedEvents(events);
|
|
67
68
|
for (let event of parsedEvents) {
|
|
@@ -69,7 +70,7 @@ class EVMContractEvents extends EVMEvents_1.EVMEvents {
|
|
|
69
70
|
if (result != null)
|
|
70
71
|
return result;
|
|
71
72
|
}
|
|
72
|
-
}, abortSignal, this.contract.contractDeploymentHeight);
|
|
73
|
+
}, abortSignal, Math.max(this.contract.contractDeploymentHeight, startHeight ?? 0));
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
exports.EVMContractEvents = EVMContractEvents;
|
|
@@ -36,7 +36,7 @@ class ReconnectingWebSocketProvider extends SocketProvider_1.SocketProvider {
|
|
|
36
36
|
logger.info("connect(): Websocket connected!");
|
|
37
37
|
};
|
|
38
38
|
this.websocket.onerror = (err) => {
|
|
39
|
-
logger.error(`connect(): onerror: Websocket connection error: `, err);
|
|
39
|
+
logger.error(`connect(): onerror: Websocket connection error: `, err.error ?? err);
|
|
40
40
|
this.disconnectedAndScheduleReconnect();
|
|
41
41
|
};
|
|
42
42
|
this.websocket.onmessage = (message) => {
|
|
@@ -59,7 +59,7 @@ class ReconnectingWebSocketProvider extends SocketProvider_1.SocketProvider {
|
|
|
59
59
|
return;
|
|
60
60
|
this.websocket.onclose = null;
|
|
61
61
|
//Register dummy handler, otherwise we get unhandled `error` event which crashes the whole thing
|
|
62
|
-
this.websocket.onerror = (err) => logger.error("disconnectedAndScheduleReconnect(): Post-close onerror: ", err);
|
|
62
|
+
this.websocket.onerror = (err) => logger.error("disconnectedAndScheduleReconnect(): Post-close onerror: ", err.error ?? err);
|
|
63
63
|
this.websocket.onmessage = null;
|
|
64
64
|
this.websocket.onopen = null;
|
|
65
65
|
this.websocket = null;
|
|
@@ -40,7 +40,7 @@ export declare class EVMSpvVaultContract<ChainId extends string> extends EVMCont
|
|
|
40
40
|
protected Claim(signer: string, vault: EVMSpvVaultData, data: EVMSpvWithdrawalData, blockheader: EVMBtcStoredHeader, merkle: Buffer[], position: number, feeRate: string): Promise<TransactionRequest>;
|
|
41
41
|
checkWithdrawalTx(tx: SpvWithdrawalTransactionData): Promise<void>;
|
|
42
42
|
createVaultData(owner: string, vaultId: bigint, utxo: string, confirmations: number, tokenData: SpvVaultTokenData[]): Promise<EVMSpvVaultData>;
|
|
43
|
-
getFronterAddress(owner: string, vaultId: bigint, withdrawal: EVMSpvWithdrawalData): Promise<string>;
|
|
43
|
+
getFronterAddress(owner: string, vaultId: bigint, withdrawal: EVMSpvWithdrawalData): Promise<string | null>;
|
|
44
44
|
getFronterAddresses(withdrawals: {
|
|
45
45
|
owner: string;
|
|
46
46
|
vaultId: bigint;
|
|
@@ -69,8 +69,11 @@ export declare class EVMSpvVaultContract<ChainId extends string> extends EVMCont
|
|
|
69
69
|
}>;
|
|
70
70
|
getAllVaults(owner?: string): Promise<EVMSpvVaultData[]>;
|
|
71
71
|
private parseWithdrawalEvent;
|
|
72
|
-
getWithdrawalState(
|
|
73
|
-
getWithdrawalStates(
|
|
72
|
+
getWithdrawalState(withdrawalTx: EVMSpvWithdrawalData, scStartHeight?: number): Promise<SpvWithdrawalState>;
|
|
73
|
+
getWithdrawalStates(withdrawalTxs: {
|
|
74
|
+
withdrawal: EVMSpvWithdrawalData;
|
|
75
|
+
scStartBlockheight?: number;
|
|
76
|
+
}[]): Promise<{
|
|
74
77
|
[btcTxId: string]: SpvWithdrawalState;
|
|
75
78
|
}>;
|
|
76
79
|
getWithdrawalData(btcTx: BtcTx): Promise<EVMSpvWithdrawalData>;
|
|
@@ -262,49 +262,100 @@ class EVMSpvVaultContract extends EVMContractBase_1.EVMContractBase {
|
|
|
262
262
|
return null;
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
|
-
async getWithdrawalState(
|
|
266
|
-
const txHash = buffer_1.Buffer.from(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
265
|
+
async getWithdrawalState(withdrawalTx, scStartHeight) {
|
|
266
|
+
const txHash = buffer_1.Buffer.from(withdrawalTx.getTxId(), "hex").reverse();
|
|
267
|
+
const events = ["Fronted", "Claimed", "Closed"];
|
|
268
|
+
const keys = [null, null, (0, ethers_1.hexlify)(txHash)];
|
|
269
|
+
let result;
|
|
270
|
+
if (scStartHeight == null) {
|
|
271
|
+
result = await this.Events.findInContractEvents(events, keys, async (event) => {
|
|
272
|
+
const result = this.parseWithdrawalEvent(event);
|
|
273
|
+
if (result != null)
|
|
274
|
+
return result;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
result = await this.Events.findInContractEventsForward(events, keys, async (event) => {
|
|
279
|
+
const result = this.parseWithdrawalEvent(event);
|
|
280
|
+
if (result == null)
|
|
281
|
+
return;
|
|
282
|
+
if (result.type === base_1.SpvWithdrawalStateType.FRONTED) {
|
|
283
|
+
//Check if still fronted
|
|
284
|
+
const fronterAddress = await this.getFronterAddress(result.owner, result.vaultId, withdrawalTx);
|
|
285
|
+
//Not fronted now, there should be a claim/close event after the front event, continue
|
|
286
|
+
if (fronterAddress == null)
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
274
289
|
return result;
|
|
275
|
-
|
|
290
|
+
}, scStartHeight);
|
|
291
|
+
}
|
|
276
292
|
result ?? (result = {
|
|
277
293
|
type: base_1.SpvWithdrawalStateType.NOT_FOUND
|
|
278
294
|
});
|
|
279
295
|
return result;
|
|
280
296
|
}
|
|
281
|
-
async getWithdrawalStates(
|
|
297
|
+
async getWithdrawalStates(withdrawalTxs) {
|
|
298
|
+
var _a;
|
|
282
299
|
const result = {};
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const btcTxId = buffer_1.Buffer.from(_event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
|
|
293
|
-
if (!checkBtcTxIdsSet.has(btcTxId)) {
|
|
294
|
-
this.logger.warn(`getWithdrawalStates(): findInContractEvents-callback: loaded event for ${btcTxId}, but transaction not found in input params!`);
|
|
295
|
-
return null;
|
|
300
|
+
const events = ["Fronted", "Claimed", "Closed"];
|
|
301
|
+
for (let i = 0; i < withdrawalTxs.length; i += this.Chain.config.maxLogTopics) {
|
|
302
|
+
const checkWithdrawalTxs = withdrawalTxs.slice(i, i + this.Chain.config.maxLogTopics);
|
|
303
|
+
const checkWithdrawalTxsMap = new Map(checkWithdrawalTxs.map(val => [val.withdrawal.getTxId(), val.withdrawal]));
|
|
304
|
+
let scStartHeight = null;
|
|
305
|
+
for (let val of checkWithdrawalTxs) {
|
|
306
|
+
if (val.scStartBlockheight == null) {
|
|
307
|
+
scStartHeight = null;
|
|
308
|
+
break;
|
|
296
309
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
310
|
+
scStartHeight = Math.min(scStartHeight ?? Infinity, val.scStartBlockheight);
|
|
311
|
+
}
|
|
312
|
+
const keys = [null, null, checkWithdrawalTxs.map(withdrawal => (0, ethers_1.hexlify)(buffer_1.Buffer.from(withdrawal.withdrawal.getTxId(), "hex").reverse()))];
|
|
313
|
+
if (scStartHeight == null) {
|
|
314
|
+
await this.Events.findInContractEvents(events, keys, async (event) => {
|
|
315
|
+
const _event = event;
|
|
316
|
+
const btcTxId = buffer_1.Buffer.from(_event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
|
|
317
|
+
if (!checkWithdrawalTxsMap.has(btcTxId)) {
|
|
318
|
+
this.logger.warn(`getWithdrawalStates(): findInContractEvents-callback: loaded event for ${btcTxId}, but transaction not found in input params!`);
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
const eventResult = this.parseWithdrawalEvent(event);
|
|
322
|
+
if (eventResult == null)
|
|
323
|
+
return null;
|
|
324
|
+
checkWithdrawalTxsMap.delete(btcTxId);
|
|
325
|
+
result[btcTxId] = eventResult;
|
|
326
|
+
if (checkWithdrawalTxsMap.size === 0)
|
|
327
|
+
return true; //All processed
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
await this.Events.findInContractEventsForward(events, keys, async (event) => {
|
|
332
|
+
const _event = event;
|
|
333
|
+
const btcTxId = buffer_1.Buffer.from(_event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
|
|
334
|
+
const withdrawalTx = checkWithdrawalTxsMap.get(btcTxId);
|
|
335
|
+
if (withdrawalTx == null) {
|
|
336
|
+
this.logger.warn(`getWithdrawalStates(): findInContractEvents-callback: loaded event for ${btcTxId}, but transaction not found in input params!`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const eventResult = this.parseWithdrawalEvent(event);
|
|
340
|
+
if (eventResult == null)
|
|
341
|
+
return;
|
|
342
|
+
if (eventResult.type === base_1.SpvWithdrawalStateType.FRONTED) {
|
|
343
|
+
//Check if still fronted
|
|
344
|
+
const fronterAddress = await this.getFronterAddress(eventResult.owner, eventResult.vaultId, withdrawalTx);
|
|
345
|
+
//Not fronted now, so there should be a claim/close event after the front event, continue
|
|
346
|
+
if (fronterAddress == null)
|
|
347
|
+
return;
|
|
348
|
+
//Fronted still, so this should be the latest current state
|
|
349
|
+
}
|
|
350
|
+
checkWithdrawalTxsMap.delete(btcTxId);
|
|
351
|
+
result[btcTxId] = eventResult;
|
|
352
|
+
if (checkWithdrawalTxsMap.size === 0)
|
|
353
|
+
return true; //All processed
|
|
354
|
+
}, scStartHeight);
|
|
355
|
+
}
|
|
305
356
|
}
|
|
306
|
-
for (let
|
|
307
|
-
result[
|
|
357
|
+
for (let val of withdrawalTxs) {
|
|
358
|
+
result[_a = val.withdrawal.getTxId()] ?? (result[_a] = {
|
|
308
359
|
type: base_1.SpvWithdrawalStateType.NOT_FOUND
|
|
309
360
|
});
|
|
310
361
|
}
|
|
@@ -22,6 +22,7 @@ export declare class EVMPersistentSigner extends EVMSigner {
|
|
|
22
22
|
private save;
|
|
23
23
|
private checkPastTransactions;
|
|
24
24
|
private startFeeBumper;
|
|
25
|
+
private syncNonceFromChain;
|
|
25
26
|
init(): Promise<void>;
|
|
26
27
|
stop(): Promise<void>;
|
|
27
28
|
private readonly sendTransactionQueue;
|
|
@@ -74,12 +74,14 @@ class EVMPersistentSigner extends EVMSigner_1.EVMSigner {
|
|
|
74
74
|
let _safeBlockTxCount = null;
|
|
75
75
|
for (let [nonce, data] of this.pendingTxs) {
|
|
76
76
|
if (!data.sending && data.lastBumped < Date.now() - this.waitBeforeBump) {
|
|
77
|
-
_safeBlockTxCount
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
if (_safeBlockTxCount == null) {
|
|
78
|
+
_safeBlockTxCount = await this.chainInterface.provider.getTransactionCount(this.address, this.safeBlockTag);
|
|
79
|
+
this.confirmedNonce = _safeBlockTxCount - 1;
|
|
80
|
+
}
|
|
81
|
+
if (this.confirmedNonce >= nonce) {
|
|
80
82
|
this.pendingTxs.delete(nonce);
|
|
81
83
|
data.txs.forEach(tx => this.chainInterface.Transactions._knownTxSet.delete(tx.hash));
|
|
82
|
-
this.logger.info(
|
|
84
|
+
this.logger.info(`checkPastTransactions(): Tx confirmed, nonce: ${nonce}, required fee bumps: `, data.txs.length);
|
|
83
85
|
this.save();
|
|
84
86
|
continue;
|
|
85
87
|
}
|
|
@@ -159,6 +161,22 @@ class EVMPersistentSigner extends EVMSigner_1.EVMSigner {
|
|
|
159
161
|
};
|
|
160
162
|
func();
|
|
161
163
|
}
|
|
164
|
+
async syncNonceFromChain() {
|
|
165
|
+
const txCount = await this.chainInterface.provider.getTransactionCount(this.address, this.safeBlockTag);
|
|
166
|
+
this.confirmedNonce = txCount - 1;
|
|
167
|
+
if (this.pendingNonce < this.confirmedNonce) {
|
|
168
|
+
this.logger.info(`syncNonceFromChain(): Re-synced latest nonce from chain, adjusting local pending nonce ${this.pendingNonce} -> ${this.confirmedNonce}`);
|
|
169
|
+
this.pendingNonce = this.confirmedNonce;
|
|
170
|
+
for (let [nonce, data] of this.pendingTxs) {
|
|
171
|
+
if (nonce <= this.pendingNonce) {
|
|
172
|
+
this.pendingTxs.delete(nonce);
|
|
173
|
+
data.txs.forEach(tx => this.chainInterface.Transactions._knownTxSet.delete(tx.hash));
|
|
174
|
+
this.logger.info(`syncNonceFromChain(): Tx confirmed, nonce: ${nonce}, required fee bumps: `, data.txs.length);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
this.save();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
162
180
|
async init() {
|
|
163
181
|
try {
|
|
164
182
|
await fs.mkdir(this.directory);
|
|
@@ -213,11 +231,18 @@ class EVMPersistentSigner extends EVMSigner_1.EVMSigner {
|
|
|
213
231
|
this.save();
|
|
214
232
|
this.chainInterface.Transactions._knownTxSet.add(signedTx.hash);
|
|
215
233
|
try {
|
|
234
|
+
//TODO: This can fail due to not receiving a response from the server, however the transaction
|
|
235
|
+
// might already be broadcasted!
|
|
216
236
|
const result = await this.chainInterface.provider.broadcastTransaction(signedRawTx);
|
|
217
237
|
pendingTxObject.sending = false;
|
|
218
238
|
return result;
|
|
219
239
|
}
|
|
220
240
|
catch (e) {
|
|
241
|
+
if (e.code === "NONCE_EXPIRED") {
|
|
242
|
+
//Re-check nonce from on-chain
|
|
243
|
+
this.logger.info("sendTransaction(): Got NONCE_EXPIRED back from backend, re-checking latest nonce from chain!");
|
|
244
|
+
await this.syncNonceFromChain();
|
|
245
|
+
}
|
|
221
246
|
this.chainInterface.Transactions._knownTxSet.delete(signedTx.hash);
|
|
222
247
|
this.pendingTxs.delete(transaction.nonce);
|
|
223
248
|
this.pendingNonce--;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atomiqlabs/chain-evm",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "EVM specific base implementation",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types:": "./dist/index.d.ts",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"author": "adambor",
|
|
24
24
|
"license": "Apache-2.0",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@atomiqlabs/base": "^10.0.0-dev.
|
|
26
|
+
"@atomiqlabs/base": "^10.0.0-dev.21",
|
|
27
27
|
"@noble/hashes": "^1.8.0",
|
|
28
28
|
"@scure/btc-signer": "^1.6.0",
|
|
29
29
|
"buffer": "6.0.3",
|
|
@@ -87,7 +87,7 @@ export type BotanixOptions = {
|
|
|
87
87
|
|
|
88
88
|
fees?: EVMFees,
|
|
89
89
|
|
|
90
|
-
evmConfig?: Omit<EVMConfiguration, "safeBlockTag">
|
|
90
|
+
evmConfig?: Omit<EVMConfiguration, "safeBlockTag" | "finalizedBlockTag">
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
export function initializeBotanix(
|
|
@@ -121,6 +121,7 @@ export function initializeBotanix(
|
|
|
121
121
|
|
|
122
122
|
const chainInterface = new EVMChainInterface("BOTANIX", chainId, provider, {
|
|
123
123
|
safeBlockTag: "finalized",
|
|
124
|
+
finalizedBlockTag: "finalized",
|
|
124
125
|
maxLogsBlockRange: options?.evmConfig?.maxLogsBlockRange ?? 950,
|
|
125
126
|
maxLogTopics: options?.evmConfig?.maxLogTopics ?? 64,
|
|
126
127
|
maxParallelLogRequests: options?.evmConfig?.maxParallelLogRequests ?? 5,
|
|
@@ -93,7 +93,7 @@ export type CitreaOptions = {
|
|
|
93
93
|
|
|
94
94
|
fees?: CitreaFees,
|
|
95
95
|
|
|
96
|
-
evmConfig?: Omit<EVMConfiguration, "safeBlockTag">
|
|
96
|
+
evmConfig?: Omit<EVMConfiguration, "safeBlockTag" | "finalizedBlockTag">
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
export function initializeCitrea(
|
|
@@ -127,6 +127,7 @@ export function initializeCitrea(
|
|
|
127
127
|
|
|
128
128
|
const chainInterface = new EVMChainInterface("CITREA", chainId, provider, {
|
|
129
129
|
safeBlockTag: "latest",
|
|
130
|
+
finalizedBlockTag: "safe",
|
|
130
131
|
maxLogsBlockRange: options?.evmConfig?.maxLogsBlockRange ?? 950,
|
|
131
132
|
maxLogTopics: options?.evmConfig?.maxLogTopics ?? 64,
|
|
132
133
|
maxParallelLogRequests: options?.evmConfig?.maxParallelLogRequests ?? 5,
|
|
@@ -28,6 +28,7 @@ export type EVMRetryPolicy = {
|
|
|
28
28
|
|
|
29
29
|
export type EVMConfiguration = {
|
|
30
30
|
safeBlockTag: EVMBlockTag,
|
|
31
|
+
finalizedBlockTag: EVMBlockTag,
|
|
31
32
|
maxLogsBlockRange: number,
|
|
32
33
|
maxParallelLogRequests: number,
|
|
33
34
|
maxParallelCalls: number,
|
|
@@ -68,6 +69,7 @@ export class EVMChainInterface<ChainId extends string = string> implements Chain
|
|
|
68
69
|
this.retryPolicy = retryPolicy;
|
|
69
70
|
this.config = config;
|
|
70
71
|
this.config.safeBlockTag ??= "safe";
|
|
72
|
+
this.config.finalizedBlockTag ??= "finalized";
|
|
71
73
|
|
|
72
74
|
this.logger = getLogger("EVMChainInterface("+this.evmChainId+"): ");
|
|
73
75
|
|
|
@@ -154,6 +156,14 @@ export class EVMChainInterface<ChainId extends string = string> implements Chain
|
|
|
154
156
|
return this.Transactions.getTxStatus(tx);
|
|
155
157
|
}
|
|
156
158
|
|
|
159
|
+
async getFinalizedBlock(): Promise<{ height: number; blockHash: string }> {
|
|
160
|
+
const block = await this.Blocks.getBlock(this.config.finalizedBlockTag);
|
|
161
|
+
return {
|
|
162
|
+
height: block.number,
|
|
163
|
+
blockHash: block.hash
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
157
167
|
async txsTransfer(signer: string, token: string, amount: bigint, dstAddress: string, feeRate?: string): Promise<TransactionRequest[]> {
|
|
158
168
|
return [await this.Tokens.Transfer(signer, token, amount, dstAddress, feeRate)];
|
|
159
169
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {EVMModule} from "../EVMModule";
|
|
2
|
+
import {Block} from "ethers";
|
|
2
3
|
|
|
3
4
|
export type EVMBlockTag = "safe" | "pending" | "latest" | "finalized";
|
|
4
5
|
|
|
@@ -8,7 +9,7 @@ export class EVMBlocks extends EVMModule<any> {
|
|
|
8
9
|
|
|
9
10
|
private blockCache: {
|
|
10
11
|
[key: string]: {
|
|
11
|
-
|
|
12
|
+
block: Promise<Block>,
|
|
12
13
|
timestamp: number
|
|
13
14
|
}
|
|
14
15
|
} = {};
|
|
@@ -20,23 +21,23 @@ export class EVMBlocks extends EVMModule<any> {
|
|
|
20
21
|
* @param blockTag
|
|
21
22
|
*/
|
|
22
23
|
private fetchAndSaveBlockTime(blockTag: EVMBlockTag | number): {
|
|
23
|
-
|
|
24
|
+
block: Promise<Block>,
|
|
24
25
|
timestamp: number
|
|
25
26
|
} {
|
|
26
27
|
const blockTagStr = blockTag.toString(10);
|
|
27
28
|
|
|
28
|
-
const
|
|
29
|
+
const blockPromise = this.provider.getBlock(blockTag, false);
|
|
29
30
|
const timestamp = Date.now();
|
|
30
31
|
this.blockCache[blockTagStr] = {
|
|
31
|
-
|
|
32
|
+
block: blockPromise,
|
|
32
33
|
timestamp
|
|
33
34
|
};
|
|
34
|
-
|
|
35
|
-
if(this.blockCache[blockTagStr]!=null && this.blockCache[blockTagStr].
|
|
35
|
+
blockPromise.catch(e => {
|
|
36
|
+
if(this.blockCache[blockTagStr]!=null && this.blockCache[blockTagStr].block===blockPromise) delete this.blockCache[blockTagStr];
|
|
36
37
|
throw e;
|
|
37
38
|
})
|
|
38
39
|
return {
|
|
39
|
-
|
|
40
|
+
block: blockPromise,
|
|
40
41
|
timestamp
|
|
41
42
|
};
|
|
42
43
|
}
|
|
@@ -61,7 +62,7 @@ export class EVMBlocks extends EVMModule<any> {
|
|
|
61
62
|
*
|
|
62
63
|
* @param blockTag
|
|
63
64
|
*/
|
|
64
|
-
public
|
|
65
|
+
public getBlock(blockTag: EVMBlockTag | number): Promise<Block> {
|
|
65
66
|
this.cleanupBlocks();
|
|
66
67
|
let cachedBlockData = this.blockCache[blockTag.toString(10)];
|
|
67
68
|
|
|
@@ -69,7 +70,17 @@ export class EVMBlocks extends EVMModule<any> {
|
|
|
69
70
|
cachedBlockData = this.fetchAndSaveBlockTime(blockTag);
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
return cachedBlockData.
|
|
73
|
+
return cachedBlockData.block;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Gets the block time for a given blocktag, with caching
|
|
78
|
+
*
|
|
79
|
+
* @param blockTag
|
|
80
|
+
*/
|
|
81
|
+
public async getBlockTime(blockTag: EVMBlockTag | number): Promise<number> {
|
|
82
|
+
const block = await this.getBlock(blockTag);
|
|
83
|
+
return block.timestamp;
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
}
|
|
@@ -24,6 +24,7 @@ export class EVMEvents extends EVMModule<any> {
|
|
|
24
24
|
});
|
|
25
25
|
} catch (e) {
|
|
26
26
|
if(
|
|
27
|
+
(e.error?.code===-32602 && e.error?.message?.startsWith("query exceeds max results")) || //Query exceeds max results
|
|
27
28
|
e.error?.code===-32008 || //Response is too big
|
|
28
29
|
e.error?.code===-32005 //Limit exceeded
|
|
29
30
|
) {
|
|
@@ -2,6 +2,7 @@ import {EVMModule} from "../EVMModule";
|
|
|
2
2
|
import {Transaction, TransactionRequest, TransactionResponse} from "ethers";
|
|
3
3
|
import {timeoutPromise} from "../../../utils/Utils";
|
|
4
4
|
import {EVMSigner} from "../../wallet/EVMSigner";
|
|
5
|
+
import {TransactionRevertedError} from "@atomiqlabs/base";
|
|
5
6
|
|
|
6
7
|
export type EVMTx = TransactionRequest;
|
|
7
8
|
|
|
@@ -87,7 +88,7 @@ export class EVMTransactions extends EVMModule<any> {
|
|
|
87
88
|
if(currentConfirmedNonce==null || nextAccountNonce > currentConfirmedNonce) {
|
|
88
89
|
this.latestConfirmedNonces[tx.from] = nextAccountNonce;
|
|
89
90
|
}
|
|
90
|
-
if(state==="reverted") throw new
|
|
91
|
+
if(state==="reverted") throw new TransactionRevertedError("Transaction reverted!");
|
|
91
92
|
|
|
92
93
|
return confirmedTxId;
|
|
93
94
|
}
|
|
@@ -81,12 +81,14 @@ export class EVMContractEvents<T extends BaseContract> extends EVMEvents {
|
|
|
81
81
|
* @param keys
|
|
82
82
|
* @param processor called for every event, should return a value if the correct event was found, or null
|
|
83
83
|
* if the search should continue
|
|
84
|
+
* @param startHeight
|
|
84
85
|
* @param abortSignal
|
|
85
86
|
*/
|
|
86
87
|
public async findInContractEventsForward<TResult, TEventName extends keyof T["filters"]>(
|
|
87
88
|
events: TEventName[],
|
|
88
89
|
keys: (string | string[])[],
|
|
89
90
|
processor: (event: TypedEventLog<T["filters"][TEventName]>) => Promise<TResult>,
|
|
91
|
+
startHeight?: number,
|
|
90
92
|
abortSignal?: AbortSignal
|
|
91
93
|
): Promise<TResult> {
|
|
92
94
|
return this.findInEventsForward<TResult>(await this.baseContract.getAddress(), this.toFilter(events, keys), async (events: Log[]) => {
|
|
@@ -95,7 +97,7 @@ export class EVMContractEvents<T extends BaseContract> extends EVMEvents {
|
|
|
95
97
|
const result: TResult = await processor(event);
|
|
96
98
|
if(result!=null) return result;
|
|
97
99
|
}
|
|
98
|
-
}, abortSignal, this.contract.contractDeploymentHeight);
|
|
100
|
+
}, abortSignal, Math.max(this.contract.contractDeploymentHeight, startHeight ?? 0));
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
}
|
|
@@ -51,7 +51,7 @@ export class ReconnectingWebSocketProvider extends SocketProvider {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
this.websocket.onerror = (err) => {
|
|
54
|
-
logger.error(`connect(): onerror: Websocket connection error: `, err);
|
|
54
|
+
logger.error(`connect(): onerror: Websocket connection error: `, err.error ?? err);
|
|
55
55
|
this.disconnectedAndScheduleReconnect();
|
|
56
56
|
};
|
|
57
57
|
|
|
@@ -76,7 +76,7 @@ export class ReconnectingWebSocketProvider extends SocketProvider {
|
|
|
76
76
|
if(this.websocket==null) return;
|
|
77
77
|
this.websocket.onclose = null;
|
|
78
78
|
//Register dummy handler, otherwise we get unhandled `error` event which crashes the whole thing
|
|
79
|
-
this.websocket.onerror = (err) => logger.error("disconnectedAndScheduleReconnect(): Post-close onerror: ", err);
|
|
79
|
+
this.websocket.onerror = (err) => logger.error("disconnectedAndScheduleReconnect(): Post-close onerror: ", err.error ?? err);
|
|
80
80
|
this.websocket.onmessage = null;
|
|
81
81
|
this.websocket.onopen = null;
|
|
82
82
|
this.websocket = null;
|
|
@@ -11,9 +11,9 @@ import {
|
|
|
11
11
|
TransactionConfirmationOptions
|
|
12
12
|
} from "@atomiqlabs/base";
|
|
13
13
|
import {Buffer} from "buffer";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
14
|
+
import {EVMTx} from "../chain/modules/EVMTransactions";
|
|
15
|
+
import {EVMContractBase} from "../contract/EVMContractBase";
|
|
16
|
+
import {EVMSigner} from "../wallet/EVMSigner";
|
|
17
17
|
import {SpvVaultContractAbi} from "./SpvVaultContractAbi";
|
|
18
18
|
import {SpvVaultManager, SpvVaultParametersStructOutput} from "./SpvVaultContractTypechain";
|
|
19
19
|
import {EVMBtcRelay} from "../btcrelay/EVMBtcRelay";
|
|
@@ -204,7 +204,7 @@ export class EVMSpvVaultContract<ChainId extends string>
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
//Getters
|
|
207
|
-
async getFronterAddress(owner: string, vaultId: bigint, withdrawal: EVMSpvWithdrawalData): Promise<string> {
|
|
207
|
+
async getFronterAddress(owner: string, vaultId: bigint, withdrawal: EVMSpvWithdrawalData): Promise<string | null> {
|
|
208
208
|
const frontingAddress = await this.contract.getFronterById(owner, vaultId, "0x"+withdrawal.getFrontingId());
|
|
209
209
|
if(frontingAddress===ZeroAddress) return null;
|
|
210
210
|
return frontingAddress;
|
|
@@ -370,58 +370,114 @@ export class EVMSpvVaultContract<ChainId extends string>
|
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
async getWithdrawalState(
|
|
374
|
-
const txHash = Buffer.from(
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
373
|
+
async getWithdrawalState(withdrawalTx: EVMSpvWithdrawalData, scStartHeight?: number): Promise<SpvWithdrawalState> {
|
|
374
|
+
const txHash = Buffer.from(withdrawalTx.getTxId(), "hex").reverse();
|
|
375
|
+
|
|
376
|
+
const events: ["Fronted", "Claimed", "Closed"] = ["Fronted", "Claimed", "Closed"];
|
|
377
|
+
const keys = [null, null, hexlify(txHash)];
|
|
378
|
+
|
|
379
|
+
let result: SpvWithdrawalState;
|
|
380
|
+
if(scStartHeight==null) {
|
|
381
|
+
result = await this.Events.findInContractEvents(
|
|
382
|
+
events, keys,
|
|
383
|
+
async (event) => {
|
|
384
|
+
const result = this.parseWithdrawalEvent(event);
|
|
385
|
+
if(result!=null) return result;
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
} else {
|
|
389
|
+
result = await this.Events.findInContractEventsForward(
|
|
390
|
+
events, keys,
|
|
391
|
+
async (event) => {
|
|
392
|
+
const result = this.parseWithdrawalEvent(event);
|
|
393
|
+
if(result==null) return;
|
|
394
|
+
if(result.type===SpvWithdrawalStateType.FRONTED) {
|
|
395
|
+
//Check if still fronted
|
|
396
|
+
const fronterAddress = await this.getFronterAddress(result.owner, result.vaultId, withdrawalTx);
|
|
397
|
+
//Not fronted now, there should be a claim/close event after the front event, continue
|
|
398
|
+
if(fronterAddress==null) return;
|
|
399
|
+
}
|
|
400
|
+
return result;
|
|
401
|
+
},
|
|
402
|
+
scStartHeight
|
|
403
|
+
);
|
|
404
|
+
}
|
|
387
405
|
result ??= {
|
|
388
406
|
type: SpvWithdrawalStateType.NOT_FOUND
|
|
389
407
|
};
|
|
390
408
|
return result;
|
|
391
409
|
}
|
|
392
410
|
|
|
393
|
-
async getWithdrawalStates(
|
|
411
|
+
async getWithdrawalStates(withdrawalTxs: {withdrawal: EVMSpvWithdrawalData, scStartBlockheight?: number}[]): Promise<{[btcTxId: string]: SpvWithdrawalState}> {
|
|
394
412
|
const result: {[btcTxId: string]: SpvWithdrawalState} = {};
|
|
395
413
|
|
|
396
|
-
|
|
397
|
-
const checkBtcTxIds = btcTxIds.slice(i, i+this.Chain.config.maxLogTopics);
|
|
398
|
-
const checkBtcTxIdsSet = new Set(checkBtcTxIds);
|
|
414
|
+
const events: ["Fronted", "Claimed", "Closed"] = ["Fronted", "Claimed", "Closed"];
|
|
399
415
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const btcTxId = Buffer.from(_event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
|
|
410
|
-
if(!checkBtcTxIdsSet.has(btcTxId)) {
|
|
411
|
-
this.logger.warn(`getWithdrawalStates(): findInContractEvents-callback: loaded event for ${btcTxId}, but transaction not found in input params!`)
|
|
412
|
-
return null;
|
|
413
|
-
}
|
|
414
|
-
const eventResult = this.parseWithdrawalEvent(event);
|
|
415
|
-
if(eventResult==null) return null;
|
|
416
|
-
checkBtcTxIdsSet.delete(btcTxId);
|
|
417
|
-
result[btcTxId] = eventResult;
|
|
418
|
-
if(checkBtcTxIdsSet.size===0) return true; //All processed
|
|
416
|
+
for(let i=0;i<withdrawalTxs.length;i+=this.Chain.config.maxLogTopics) {
|
|
417
|
+
const checkWithdrawalTxs = withdrawalTxs.slice(i, i+this.Chain.config.maxLogTopics);
|
|
418
|
+
const checkWithdrawalTxsMap = new Map(checkWithdrawalTxs.map(val => [val.withdrawal.getTxId() as string, val.withdrawal]));
|
|
419
|
+
|
|
420
|
+
let scStartHeight = null;
|
|
421
|
+
for(let val of checkWithdrawalTxs) {
|
|
422
|
+
if(val.scStartBlockheight==null) {
|
|
423
|
+
scStartHeight = null;
|
|
424
|
+
break;
|
|
419
425
|
}
|
|
420
|
-
|
|
426
|
+
scStartHeight = Math.min(scStartHeight ?? Infinity, val.scStartBlockheight);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const keys = [null, null, checkWithdrawalTxs.map(withdrawal => hexlify(Buffer.from(withdrawal.withdrawal.getTxId(), "hex").reverse()))];
|
|
430
|
+
|
|
431
|
+
if(scStartHeight==null) {
|
|
432
|
+
await this.Events.findInContractEvents(
|
|
433
|
+
events, keys,
|
|
434
|
+
async (event) => {
|
|
435
|
+
const _event = event as TypedEventLog<SpvVaultManager["filters"]["Fronted" | "Claimed" | "Closed"]>;
|
|
436
|
+
const btcTxId = Buffer.from(_event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
|
|
437
|
+
if(!checkWithdrawalTxsMap.has(btcTxId)) {
|
|
438
|
+
this.logger.warn(`getWithdrawalStates(): findInContractEvents-callback: loaded event for ${btcTxId}, but transaction not found in input params!`)
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
const eventResult = this.parseWithdrawalEvent(event);
|
|
442
|
+
if(eventResult==null) return null;
|
|
443
|
+
checkWithdrawalTxsMap.delete(btcTxId);
|
|
444
|
+
result[btcTxId] = eventResult;
|
|
445
|
+
if(checkWithdrawalTxsMap.size===0) return true; //All processed
|
|
446
|
+
}
|
|
447
|
+
);
|
|
448
|
+
} else {
|
|
449
|
+
await this.Events.findInContractEventsForward(
|
|
450
|
+
events, keys,
|
|
451
|
+
async (event) => {
|
|
452
|
+
const _event = event as TypedEventLog<SpvVaultManager["filters"]["Fronted" | "Claimed" | "Closed"]>;
|
|
453
|
+
const btcTxId = Buffer.from(_event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
|
|
454
|
+
const withdrawalTx = checkWithdrawalTxsMap.get(btcTxId);
|
|
455
|
+
if(withdrawalTx==null) {
|
|
456
|
+
this.logger.warn(`getWithdrawalStates(): findInContractEvents-callback: loaded event for ${btcTxId}, but transaction not found in input params!`)
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const eventResult = this.parseWithdrawalEvent(event);
|
|
460
|
+
if(eventResult==null) return;
|
|
461
|
+
|
|
462
|
+
if(eventResult.type===SpvWithdrawalStateType.FRONTED) {
|
|
463
|
+
//Check if still fronted
|
|
464
|
+
const fronterAddress = await this.getFronterAddress(eventResult.owner, eventResult.vaultId, withdrawalTx);
|
|
465
|
+
//Not fronted now, so there should be a claim/close event after the front event, continue
|
|
466
|
+
if(fronterAddress==null) return;
|
|
467
|
+
//Fronted still, so this should be the latest current state
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
checkWithdrawalTxsMap.delete(btcTxId);
|
|
471
|
+
result[btcTxId] = eventResult;
|
|
472
|
+
if(checkWithdrawalTxsMap.size===0) return true; //All processed
|
|
473
|
+
},
|
|
474
|
+
scStartHeight
|
|
475
|
+
);
|
|
476
|
+
}
|
|
421
477
|
}
|
|
422
478
|
|
|
423
|
-
for(let
|
|
424
|
-
result[
|
|
479
|
+
for(let val of withdrawalTxs) {
|
|
480
|
+
result[val.withdrawal.getTxId()] ??= {
|
|
425
481
|
type: SpvWithdrawalStateType.NOT_FOUND
|
|
426
482
|
};
|
|
427
483
|
}
|
|
@@ -130,12 +130,14 @@ export class EVMPersistentSigner extends EVMSigner {
|
|
|
130
130
|
|
|
131
131
|
for(let [nonce, data] of this.pendingTxs) {
|
|
132
132
|
if(!data.sending && data.lastBumped<Date.now()-this.waitBeforeBump) {
|
|
133
|
-
_safeBlockTxCount
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
if(_safeBlockTxCount==null) {
|
|
134
|
+
_safeBlockTxCount = await this.chainInterface.provider.getTransactionCount(this.address, this.safeBlockTag);
|
|
135
|
+
this.confirmedNonce = _safeBlockTxCount - 1;
|
|
136
|
+
}
|
|
137
|
+
if(this.confirmedNonce >= nonce) {
|
|
136
138
|
this.pendingTxs.delete(nonce);
|
|
137
139
|
data.txs.forEach(tx => this.chainInterface.Transactions._knownTxSet.delete(tx.hash));
|
|
138
|
-
this.logger.info(
|
|
140
|
+
this.logger.info(`checkPastTransactions(): Tx confirmed, nonce: ${nonce}, required fee bumps: `, data.txs.length);
|
|
139
141
|
this.save();
|
|
140
142
|
continue;
|
|
141
143
|
}
|
|
@@ -228,6 +230,23 @@ export class EVMPersistentSigner extends EVMSigner {
|
|
|
228
230
|
func();
|
|
229
231
|
}
|
|
230
232
|
|
|
233
|
+
private async syncNonceFromChain() {
|
|
234
|
+
const txCount = await this.chainInterface.provider.getTransactionCount(this.address, this.safeBlockTag);
|
|
235
|
+
this.confirmedNonce = txCount-1;
|
|
236
|
+
if(this.pendingNonce < this.confirmedNonce) {
|
|
237
|
+
this.logger.info(`syncNonceFromChain(): Re-synced latest nonce from chain, adjusting local pending nonce ${this.pendingNonce} -> ${this.confirmedNonce}`);
|
|
238
|
+
this.pendingNonce = this.confirmedNonce;
|
|
239
|
+
for(let [nonce, data] of this.pendingTxs) {
|
|
240
|
+
if(nonce <= this.pendingNonce) {
|
|
241
|
+
this.pendingTxs.delete(nonce);
|
|
242
|
+
data.txs.forEach(tx => this.chainInterface.Transactions._knownTxSet.delete(tx.hash));
|
|
243
|
+
this.logger.info(`syncNonceFromChain(): Tx confirmed, nonce: ${nonce}, required fee bumps: `, data.txs.length);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
this.save();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
231
250
|
async init(): Promise<void> {
|
|
232
251
|
try {
|
|
233
252
|
await fs.mkdir(this.directory)
|
|
@@ -291,10 +310,17 @@ export class EVMPersistentSigner extends EVMSigner {
|
|
|
291
310
|
this.chainInterface.Transactions._knownTxSet.add(signedTx.hash);
|
|
292
311
|
|
|
293
312
|
try {
|
|
313
|
+
//TODO: This can fail due to not receiving a response from the server, however the transaction
|
|
314
|
+
// might already be broadcasted!
|
|
294
315
|
const result = await this.chainInterface.provider.broadcastTransaction(signedRawTx);
|
|
295
316
|
pendingTxObject.sending = false;
|
|
296
317
|
return result;
|
|
297
318
|
} catch (e) {
|
|
319
|
+
if(e.code==="NONCE_EXPIRED") {
|
|
320
|
+
//Re-check nonce from on-chain
|
|
321
|
+
this.logger.info("sendTransaction(): Got NONCE_EXPIRED back from backend, re-checking latest nonce from chain!");
|
|
322
|
+
await this.syncNonceFromChain();
|
|
323
|
+
}
|
|
298
324
|
this.chainInterface.Transactions._knownTxSet.delete(signedTx.hash);
|
|
299
325
|
this.pendingTxs.delete(transaction.nonce);
|
|
300
326
|
this.pendingNonce--;
|