Why an SDK Falsely Reported a Transaction as Replaced
The same wallet registered an asset and then deployed a contract. Registration had already completed, and the new deployment request had been accepted. Yet the SDK waiting for the result reported that the transaction had been replaced. The supposed replacement was the earlier asset registration, which had no reason to compete with the new transaction.
This happened during acceptance testing with an actual SDK in nigo-protocol on October 15. The Ethereum transaction sequence was being maintained correctly. The problem was a query response that presented a Native transaction, processed under different rules, as an Ethereum transaction.
The article on EVM compatibility described a development workflow that reuses the same contracts with ethers, viem, and Web3j. This case shows why that compatibility must extend beyond creating transactions to the way an SDK waits for and interprets their results.
Two transactions from the same address
Let us simplify the sequence. Address A first registers an asset through Native transaction N. After N is finalized, the same A signs Ethereum Type-2 transaction E to deploy an adapter contract. N and E are explanatory names that stand in for the actual transactions' long identifiers.
The two transactions have different hashes. A hash identifies an individual signed transaction. A nonce, in contrast, is the sequence number for transactions sent by an Ethereum account. It helps govern transaction ordering and prevent replay. If the same sender submits different Ethereum transactions with the same nonce, the SDK checks which transaction was finalized. A replacement is detected when another transaction is found to have used the sequence number of the transaction being awaited.
Native and Ethereum requests in nigo-protocol can use the same address, but they do not share all the same replay rules. Native transaction N does not consume an Ethereum nonce. If A has not yet sent an Ethereum transaction, E can therefore still use nonce 0 after N completes. Waiting for N before submitting E was also the correct order.
Native registration N has already been finalized at every stage in this table.
| Stage | Deployment E | Finalized nonce |
|---|---|---|
| Registration completed | Not yet submitted | 0 |
| Deployment accepted | Pending with nonce 0 | 0 |
| Deployment finalized | Nonce 0 consumed | 1 |
The nonce column shows the value queried from the finalized Ethereum account state. It does not recommend a nonce for the next submission while accounting for pending transactions. The last row is the result confirmed in actual testing after the fix. In the initial failure, the SDK stopped waiting before E's final result was queried. The error message alone therefore did not establish that E had failed to execute or that asset settlement had gone wrong.
How a placeholder zero became a transaction sequence number
SDKs query transactions and blocks through RPC, the request-and-response interface between an app and a node. The faulty implementation included Native transaction N in the transaction list returned by Ethereum block queries. When no Ethereum nonce existed for N, it filled the field with a default value of 0.
Someone familiar with the internals could distinguish this zero from a real Ethereum sequence number. A standard SDK, however, received the same sender A, nonce 0, and a position in an already finalized block. While E was still pending, N appeared to be a transaction that had already taken E's place.
The wait() function in ethers 6.17.0 examined this block and identified N as a replacement.
At the same time, the node's account query still returned Ethereum nonce 0, and the query for E showed that it was pending.
Responses describing the same ledger were telling different stories.
Documenting that a Native transaction's nonce field should not be interpreted as a sequence number was not enough. Reusing an existing SDK also means preserving the meanings that the SDK expects from its fields. A correctly formatted number with the wrong meaning can lead an app to the wrong conclusion through an otherwise normal workflow.
Defining what belongs in Ethereum history
The fix changed Ethereum queries to expose only transactions actually signed in an Ethereum format as transaction history. The supported Legacy, Type-2, and Type-4 transactions belong in that history. Transactions signed using the protocol's own NIGO signature format are excluded even when they execute EVM code.
An Ethereum RPC query for a block containing only N therefore still returns a block at the same height and with the same hash, but with an empty transaction list. N's full contents and execution result remain available through NIGO queries and the Console. The transaction has not been deleted from the ledger or moved to another blockchain. The change defines which kind of history each interface exposes from the same ledger.
The SDK no longer encounters Native registration N as a transaction occupying Ethereum nonce 0.
It follows its existing wait() path until E is finalized and receives E's own result.
There was no need to force the Ethereum nonce to advance for Native registration or disable replacement detection in the app.
This choice also makes the query boundary clear to developers. Ethereum RPC provides the history of Ethereum transactions, while NIGO queries provide the full history, including Native transactions. The shared state used for balance, contract code, and storage queries, as well as function calls, remains intact. Presenting separate history views does not divide that state into two ledgers.
Filtering a transaction list also changes its indices and totals
Is removing Native transactions from the block list enough? Consider this illustrative mixed block, with four transactions stored in the order shown below. Here, an index is a position within the block's array, distinct from the account nonce discussed earlier.
| Transaction in the ledger | Original index | Ethereum view index |
|---|---|---|
| Native N1 | 0 | Excluded |
| Ethereum E1 | 1 | 0 |
| Native N2 | 2 | Excluded |
| Ethereum E2 | 3 | 1 |
The Ethereum list contains only E1 and E2, so its indices must run consecutively from 0 to 1. A direct lookup of E2 by hash and a query for its execution result, or receipt, must return the same position. If the list places E2 second but the receipt places it fourth, a different inconsistency appears. The implementation preserves the original ledger positions of 1 and 3 and converts them to view indices only when preparing responses.
The same rule applies to gas, the measure of execution resources used, and to event logs. An Ethereum block's gas total and a receipt's cumulative gas include only the usage of Ethereum transactions. Log indices and the summary used to search logs follow the same history boundary. Even EVM events produced by a Native transaction stay outside Ethereum history; the full records remain on the NIGO side.
This view lets Ethereum tools read nigo-protocol blocks. It does not replace the original block hashes or ledger proof structures with Ethereum's structures. The implementation also checks that the stored transaction and its block position actually match before converting the index. It does not hide inconsistent stored information behind a plausible position in the response.
Reproducing the failure and checking the fix with the same SDK
First, a small HTTP test reproduced the recorded response sequence. It controlled only the responses returned by the node, leaving the SDK's signing, broadcasting, replacement scanning, and waiting logic unchanged. The old responses caused N to be misidentified as a replacement. With the new responses, the SDK passed over the block containing only N and received E's successful result. This comparison narrowed the cause to the query responses outside the SDK.
The original sequence was then repeated with an actual node executable. After Native registration, the Type-2 deployment from the same wallet succeeded in block 2. The deployed code, successful receipt, and Ethereum nonce advancing to 1 were all checked. The workflow that had originally failed now passed without bypassing the SDK's waiting path.
Subsequent acceptance testing used the same executable to check SDK workflows and queries after normal restarts across four combinations: H2 and RocksDB storage, each with ZERO and FIXED fee policies. The tests also compared history before and after restart: Native originals remained available, while Ethereum history contained only the corresponding Ethereum-signed transactions. Completion required correcting separate test setup errors along the way and resuming the final combination after a test time limit interrupted it. This was not an uninterrupted pass from the first attempt, nor a guarantee covering every SDK or operating environment.
Compatibility extends to reading the result
The fix preserved the same wallet, the original transaction order, and the standard SDK's way of waiting for a result. What changed was the meaning of the transaction history supplied to that tool. An accurate boundary lets developers reuse familiar submission, waiting, and query code along with their contracts.
Different transaction paths can share one ledger without every transaction having to appear in a single history format. The next question is which common rules connect the assets and state they share. The follow-up article explores that connection through a unified model that treats NIGO as one of the assets.