Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
225 commits
Select commit Hold shift + click to select a range
31d98df
feat(sdk): add txFinal, final transaction errors and open-transaction…
chybisov Sep 30, 2026
474d51d
fix(sdk): flag final failures and never erase an open transaction on …
chybisov Sep 30, 2026
23e886d
fix(sdk): keep transactions with an unknown outcome on restart and cl…
chybisov Sep 30, 2026
f7840e2
feat(sdk): add isKnownToStatusApi as the status-API veto on dropped t…
chybisov Sep 30, 2026
419244b
fix(bitcoin): mark a cancelled replacement as a final outcome
chybisov Sep 30, 2026
eaf9428
fix(tron): add signed transaction serialization for txHex
chybisov Sep 30, 2026
feb15e4
fix(ethereum): mark reverted, replaced and failed receipts as final o…
chybisov Sep 30, 2026
79361b2
fix(stellar): mark a FAILED transaction result as a final outcome
chybisov Sep 30, 2026
5557503
fix(sui): add signed transaction serialization for txHex
chybisov Sep 30, 2026
a1cacca
fix(solana): tell a blockhash expiry from the confirmation ceiling an…
chybisov Sep 30, 2026
43e1518
fix(bitcoin): route resume by hasOpenTransaction and guard the sign task
chybisov Sep 30, 2026
07fdca3
fix(tron): store the signed transaction before broadcast
chybisov Sep 30, 2026
b733854
fix(ethereum): treat executed, partial and undefined batch outcomes a…
chybisov Sep 30, 2026
3140a48
fix(solana): keep the highest slot of an expiry streak
chybisov Sep 30, 2026
985aa2d
fix(stellar): make a submit rejection final only with a chain proof o…
chybisov Sep 30, 2026
a87bc35
fix(sui): reject non-canonical stored transaction bytes
chybisov Sep 30, 2026
e6db896
fix(tron): mark on-chain failures as final
chybisov Sep 30, 2026
2f90f16
fix(ethereum): mark failed and replaced Safe transactions as final ou…
chybisov Sep 30, 2026
7085fec
fix(solana): carry the expired verdict and its slot through the RPC race
chybisov Sep 30, 2026
8b2d61d
fix(tron): add broadcast outcome and dropped transaction checks
chybisov Sep 30, 2026
6fc5eb6
fix(ethereum): route resume by hasOpenTransaction in the selector and…
chybisov Sep 30, 2026
0f8cd58
fix(stellar): route resume by hasOpenTransaction and guard the sign task
chybisov Sep 30, 2026
83abee0
fix(sui): store the signed bytes before execution
chybisov Sep 30, 2026
274e65d
feat(solana): look a signature up with a canary that proves coverage …
chybisov Sep 30, 2026
17b303f
fix(ethereum): refuse to sign while the step has an open transaction
chybisov Sep 30, 2026
c4cddf6
fix(tron): count only pre-push broadcast codes as definite rejections
chybisov Sep 30, 2026
1147b75
test(ethereum): drop an inert guard assertion and pin a non-final gua…
chybisov Sep 30, 2026
b07769c
fix(sui): prove a dropped transaction with one batch lookup
chybisov Sep 30, 2026
496b8f0
fix(ethereum): clear the previous transaction fields when a new trans…
chybisov Sep 30, 2026
aec9b88
fix(tron): use java-tron's BANDWITH_ERROR spelling
chybisov Sep 30, 2026
f5a6cbe
fix(solana): add a head canary and make the lookup total and live
chybisov Sep 30, 2026
13d0b69
fix(sui): ask the next node for canaries, veto on any slot, use user-…
chybisov Sep 30, 2026
b0230e4
test(sdk-provider-ethereum): cover resume after reload and Try again …
chybisov Sep 30, 2026
818298a
fix(tron): resume the confirmation wait from the stored transaction
chybisov Sep 30, 2026
e0c9092
fix(sui): take the canary by transaction kind, not by position
chybisov Sep 30, 2026
e32454b
fix(solana): count a canary status only at the slot of its block
chybisov Sep 30, 2026
c598975
feat(solana): encode and decode the signed bytes kept in txHex
chybisov Oct 1, 2026
4adb8c3
fix(tron): resume at the confirmation wait while a transaction is open
chybisov Oct 1, 2026
1c6a31c
fix(solana): store the signed bytes before sending and resolve the wa…
chybisov Oct 1, 2026
a480ec5
fix(solana): refuse to encode an empty or mismatched stored transacti…
chybisov Oct 1, 2026
3e0e747
test(sdk-provider-tron): cover resume after reload and Try again with…
chybisov Oct 1, 2026
4eb42bd
fix(sui): resume the confirmation wait from the stored bytes
chybisov Oct 1, 2026
1b341cc
feat(solana): add the result rules and the dropped rule with the stat…
chybisov Oct 1, 2026
080312e
fix(sui): check the resend age cap before each re-execution try
chybisov Oct 1, 2026
cc1f24d
fix(sui): abort the re-execution at the age cap without AbortSignal.t…
chybisov Oct 1, 2026
cb9986a
feat(solana): resume a wait from the stored bytes or the stored hash
chybisov Oct 1, 2026
fcc2794
fix(sui): resume at the confirmation wait while a transaction is open
chybisov Oct 1, 2026
df95926
fix(solana): let the wait task stop resends at the resend age cap
chybisov Oct 1, 2026
1184cc0
test(sdk-provider-sui): cover resume after reload and Try again witho…
chybisov Oct 1, 2026
ccd9002
fix(solana): resume the standard wait from the stored bytes without r…
chybisov Oct 1, 2026
2d8b177
fix(solana): resume the Jito wait from the stored bundle without re-s…
chybisov Oct 1, 2026
014c190
fix(solana): route a resumed wait by the shape of the stored bytes
chybisov Oct 1, 2026
4d7bca3
fix(solana): resume at the wait task while a transaction may still land
chybisov Oct 1, 2026
ab4d0ea
test(sdk-provider-solana): cover resume after reload and Try again wi…
chybisov Oct 1, 2026
60b2d63
Merge branch 'feature/jumemb-79-bitcoin'
chybisov Oct 1, 2026
5f678a9
Merge branch 'feature/jumemb-79-stellar'
chybisov Oct 1, 2026
eb9b044
Merge branch 'feature/jumemb-79-ethereum'
chybisov Oct 1, 2026
7ca0ad0
Merge branch 'feature/jumemb-79-tron'
chybisov Oct 1, 2026
f60eaae
Merge branch 'feature/jumemb-79-sui'
chybisov Oct 1, 2026
57478e6
Merge branch 'feature/jumemb-79-solana'
chybisov Oct 1, 2026
c54eeb5
test: take reload snapshots from the route update hook
chybisov Oct 1, 2026
722076f
chore: add changesets for resuming without re-signing
chybisov Oct 1, 2026
c076048
fix(sdk): refuse a resend while signedAt is in the future
chybisov Oct 1, 2026
7749687
fix(sdk): give the status API veto a 10 s budget
chybisov Oct 1, 2026
8ac0c5b
fix(sdk): keep the original error code in the retry guard
chybisov Oct 1, 2026
3e7ef11
docs(sdk): tag the provider-only transaction helpers @internal
chybisov Oct 1, 2026
4eb3ed9
fix(tron): never send or drop stored bytes of another transaction
chybisov Oct 1, 2026
4297105
fix(solana): never send or drop stored bytes of another transaction
chybisov Oct 1, 2026
f8ada53
fix(stellar): never submit or finalize a stored envelope of another t…
chybisov Oct 1, 2026
fe3d012
chore(sui): delete a stale comment about a signing timeout
chybisov Oct 1, 2026
f929210
chore: note unknown old routes, internal helpers and the nonce cap in…
chybisov Oct 1, 2026
21a1bc2
test(solana): pin the resend of bytes whose signature is the stored t…
chybisov Oct 1, 2026
b98830c
fix(bitcoin): store the signed transaction before sending it
chybisov Oct 1, 2026
954e007
fix(sdk): deliver transaction data written after stopRouteExecution
chybisov Oct 1, 2026
3a3fc2c
fix(sui): report the Move error and tag rejections at the signer
chybisov Oct 1, 2026
caad001
fix(bitcoin): resend the stored transaction on resume within the age cap
chybisov Oct 1, 2026
3b1fac1
test(bitcoin): restore the selector pin for stored bytes without a hash
chybisov Oct 1, 2026
b0bc17e
fix(sui): read only a string message in parseSuiErrors
chybisov Oct 1, 2026
39dbb2c
test(sdk): pin every provider write of transaction data after a stop
chybisov Oct 1, 2026
a8807f9
fix(bitcoin): send in one round and keep conflicting-tx refusals unknown
chybisov Oct 1, 2026
6c17117
test(bitcoin): pin the resend after the wallet check and the first run
chybisov Oct 1, 2026
8d1f9f8
fix(sdk): merge late transaction data into the last ended execution
chybisov Oct 1, 2026
738fb6e
test(ethereum): stop during the wallet prompt, then reload or resume …
chybisov Oct 1, 2026
0f37022
Merge branch 'feature/jumemb-79-fu-sui'
chybisov Oct 1, 2026
32c29d6
Merge branch 'feature/jumemb-79-fu-bitcoin'
chybisov Oct 1, 2026
c7e4894
Merge branch 'feature/jumemb-79-fu-core'
chybisov Oct 1, 2026
6087fd1
docs(sdk): describe the Bitcoin resend cap and txHex lifetime
chybisov Oct 1, 2026
200fcaa
chore: note the follow-up fixes in the resume-without-resign changesets
chybisov Oct 1, 2026
31bed35
fix(sdk): copy txType when a late transaction merges
chybisov Oct 1, 2026
16d069a
fix(bitcoin): clear a send that every URL refuses for a full mempool
chybisov Oct 1, 2026
eb0c78a
test(bitcoin): pin the one-round send with the real bigmi transports
chybisov Oct 1, 2026
6ffbb12
fix(bitcoin): check for an open transaction again before signPsbt
chybisov Oct 1, 2026
f543abf
fix(sui): check for an open transaction again before signTransaction
chybisov Oct 1, 2026
8bdfc15
fix(solana): check for an open transaction again before signTransaction
chybisov Oct 1, 2026
0dbf6be
fix(tron): check for an open transaction again before signTransaction
chybisov Oct 1, 2026
4712e20
fix(stellar): check for an open transaction again before signTransaction
chybisov Oct 1, 2026
1664d95
fix(ethereum): check for an open transaction again before each wallet…
chybisov Oct 1, 2026
56c085b
chore: state the case-1 guard of the sdk changeset as the code has it
chybisov Oct 1, 2026
c06ebca
docs(sdk): note the second assertNoOpenTransaction call in each sign …
chybisov Oct 1, 2026
3eaf015
chore: name the full-mempool and fee-floor refusals in the bitcoin ch…
chybisov Oct 1, 2026
3809d27
chore: list every bitcoin send refusal that clears the stored transac…
chybisov Oct 1, 2026
9a7a924
Merge remote-tracking branch 'origin/main' into feature/jumemb-79-tx-…
chybisov Oct 1, 2026
29959f2
test(tron): add a network-level flow harness and the TRX swap flow
chybisov Oct 5, 2026
d221c23
test(sui): add a network-level flow harness and the swap flow spec
chybisov Oct 5, 2026
656aab9
test(ethereum): pin the native-token swap at the action level
chybisov Oct 5, 2026
78d392f
test(solana): add the network-level flow harness and swap spec
chybisov Oct 5, 2026
48d7768
test(tron): answer /status DONE only for a successful landing
chybisov Oct 5, 2026
6eda0c0
test(sui): record unknown SuiGrpcClient reads in the flow harness
chybisov Oct 5, 2026
401bbfe
test(tron): pin the TRC-20 swap with approval as a flow spec
chybisov Oct 5, 2026
6186997
test(ethereum): read route updates from the hook argument
chybisov Oct 5, 2026
555a41c
test(sui): add the bridge flow spec
chybisov Oct 5, 2026
d6a55fd
test(bitcoin): add a network-level flow harness and the bridge flow spec
chybisov Oct 5, 2026
2e6692c
test(stellar): add a network-level flow harness and the swap flow spec
chybisov Oct 5, 2026
05e858d
test(solana): record harness throws and copy the stored route
chybisov Oct 5, 2026
3c1b33d
test(sui): add the user rejection flow spec
chybisov Oct 5, 2026
a48a182
test(solana): pin the Jito bundle swap flow
chybisov Oct 5, 2026
50e21fd
test(ethereum): pin an ERC-20 swap with enough allowance
chybisov Oct 5, 2026
4a0d006
test(tron): record throws inside the flow fakes as unknown requests
chybisov Oct 5, 2026
24dd794
test(tron): pin the Tron bridge flow through /status
chybisov Oct 5, 2026
aaf6517
test(sui): record throws inside the flow harness fakes
chybisov Oct 5, 2026
5d447f9
test(bitcoin): stop live bigmi block polls in clearBigmiObservers
chybisov Oct 5, 2026
a50183a
test(sui): add the background execution flow spec
chybisov Oct 5, 2026
9fe7972
test(solana): pin the swap sent through write RPCs
chybisov Oct 5, 2026
96791b2
test(stellar): close the flow harness and record its own errors
chybisov Oct 5, 2026
b06ebb9
test(ethereum): pin a rejected sendTransaction and its retry
chybisov Oct 5, 2026
1bab0ba
test(ethereum): say how a throw in the status fake shows
chybisov Oct 5, 2026
86447b5
test(bitcoin): pin the repriced replacement (speed-up) flow
chybisov Oct 5, 2026
73069c5
test(tron): pin user rejection of the swap and of the approval
chybisov Oct 5, 2026
88e407e
test(sui): add the on-chain failure flow spec
chybisov Oct 5, 2026
7b3078e
test(solana): pin the send to every write RPC
chybisov Oct 5, 2026
680a987
test(solana): pin the Solana to Ethereum bridge flow
chybisov Oct 5, 2026
fc7a028
test(stellar): add the bridge flow spec
chybisov Oct 5, 2026
eb149d6
test(ethereum): pin the chain switch before the wallet prompt
chybisov Oct 5, 2026
29fefa1
test(bitcoin): pin the user rejection flow
chybisov Oct 5, 2026
d6c7583
test(tron): pin the exact payloads of the rejected and retried requests
chybisov Oct 5, 2026
a7364c9
test(stellar): add the allowance flow spec
chybisov Oct 5, 2026
7e18549
test(tron): pin where a background Tron run pauses
chybisov Oct 5, 2026
ef53148
test(solana): pin the user rejection flow
chybisov Oct 5, 2026
686085f
test(ethereum): pin the exchange-rate update, accepted and declined
chybisov Oct 5, 2026
add3d19
test(bitcoin): record a throw inside the fake network
chybisov Oct 5, 2026
5ec268d
test(stellar): give the fake simulation a consistent resource fee
chybisov Oct 5, 2026
ae81a09
test(bitcoin): name the connectors that wrap a rejection twice
chybisov Oct 5, 2026
a490f10
test(tron): pin an on-chain revert and "Try again"
chybisov Oct 5, 2026
b92faf0
test(bitcoin): pin where a background run pauses
chybisov Oct 5, 2026
f3bf642
test(stellar): pin the full approval the wallet signs
chybisov Oct 5, 2026
2f1d2cf
test(solana): pin where a background run pauses
chybisov Oct 5, 2026
829d2f2
test(bitcoin): say which fake the harness guard covers
chybisov Oct 5, 2026
7337e2f
test(solana): pin the on-chain failure flow
chybisov Oct 5, 2026
08a6c1f
test(stellar): add the user rejection flow spec
chybisov Oct 5, 2026
8de72bb
test(ethereum): add a network-level harness and pin a native swap
chybisov Oct 5, 2026
64eb7f5
test(bitcoin): pin the cancelled replacement as the on-chain failure
chybisov Oct 5, 2026
1d4d4cb
test(stellar): move the envelope decoders into the flow harness
chybisov Oct 5, 2026
f905487
test(stellar): add the background execution flow spec
chybisov Oct 5, 2026
8db53a2
test(ethereum): answer a nonce mismatch as a node does
chybisov Oct 5, 2026
a13f45a
test(ethereum): pin approve-then-swap through real viem actions
chybisov Oct 5, 2026
85a6af9
Merge branch 'test/money-path-solana'
chybisov Oct 5, 2026
d82f729
Merge branch 'test/money-path-tron'
chybisov Oct 5, 2026
c4bbc6b
Merge branch 'test/money-path-sui'
chybisov Oct 5, 2026
ca2dd88
Merge branch 'test/money-path-bitcoin'
chybisov Oct 5, 2026
3ba9fb1
test(stellar): add the on-chain failure flow spec
chybisov Oct 5, 2026
e7dca30
test(ethereum): pin that the approval names the quote's spender
chybisov Oct 5, 2026
793b434
test(ethereum): let the fake /status name its own receiving link
chybisov Oct 5, 2026
8a20a14
test(ethereum): pin a bridge from source transaction to RECEIVING_CHAIN
chybisov Oct 5, 2026
b8b4e19
Merge branch 'test/money-path-stellar'
chybisov Oct 5, 2026
48d4cba
test(ethereum): pin an on-chain revert and its retry
chybisov Oct 5, 2026
dc80066
test(ethereum): pin a background pause and the foreground resume
chybisov Oct 5, 2026
9c6519b
test(ethereum): pin a two-step route
chybisov Oct 5, 2026
76564e2
test(ethereum): name the re-quote cause and the background reads
chybisov Oct 5, 2026
1727e17
Merge branch 'test/money-path-evm'
chybisov Oct 5, 2026
89c28d3
Merge branch 'test/money-path-matrix'
chybisov Oct 5, 2026
02b04b8
test: pin the #507 behaviour changes in the money-path matrix
chybisov Oct 5, 2026
7d8a8b5
test: pin the Sui pending write before the node answers
chybisov Oct 5, 2026
c0ea7d5
test(sui): pin the node-refusal retry and accept the pending write
chybisov Oct 5, 2026
f46dbc6
test(sui): refuse with the gRPC shape in the node-refusal retry spec
chybisov Oct 5, 2026
b405498
test(solana): add the Jito bundle resume flow specs
chybisov Oct 5, 2026
a64d3b8
test(tron): pin the TRC-20 swap resume after a reload
chybisov Oct 5, 2026
3ee0059
fix(bitcoin): resend the stored transaction in one round
chybisov Oct 5, 2026
7b1e907
test(stellar): add the resume variants to the money-path matrix
chybisov Oct 5, 2026
39d2617
test(bitcoin): add the #507 resume variants as flow specs
chybisov Oct 6, 2026
45ced8d
test(stellar): drop the resume test that lands an expired envelope
chybisov Oct 6, 2026
3a68cd3
test: correct comments the #507 behaviour made stale
chybisov Oct 6, 2026
5fbdf2d
fix: free ended executions once no stopped run can write
chybisov Oct 6, 2026
b132cb7
fix(sui): bound the resume lookups with a deadline
chybisov Oct 6, 2026
beba513
test: cover a failing step in the execution record lifetime
chybisov Oct 6, 2026
50de15b
fix: stop polling /status once no execution waits for it
chybisov Oct 6, 2026
c05d8d4
fix(ethereum): end a relayed wait after 24 hours without signing again
chybisov Oct 6, 2026
b57207b
test(ethereum): pin the stopped relayed wait and its pause rule
chybisov Oct 6, 2026
d7ce8f9
fix(ethereum): log no relayer failure for a request the stop ended
chybisov Oct 6, 2026
0e66fdb
docs(ethereum): say how a stopped relayed wait ends
chybisov Oct 6, 2026
7cfb849
test: pin the pause rule and two stops of the /status wait
chybisov Oct 6, 2026
1d6fd3e
docs: correct the comments and changeset of the stopped /status wait
chybisov Oct 6, 2026
c9dec36
docs(sui): state the scope of the 10-second lookup budget
chybisov Oct 6, 2026
1a07630
fix(ethereum): resume a relayed step on the relayed lane
chybisov Oct 6, 2026
d4c5f8b
test(ethereum): pin the batched resume outcomes
chybisov Oct 6, 2026
e290e20
Merge branch 'main' into feature/jumemb-79-tx-is-trying-to-be-resubmi…
chybisov Oct 6, 2026
a65a095
test(ethereum): follow the new prepare read order in EN5
chybisov Oct 6, 2026
bd892bf
fix(sdk): do not replay a step after the route stops
chybisov Oct 6, 2026
8067c36
fix(sdk): report a refused step replay as a transaction conflict
chybisov Oct 6, 2026
a27432e
test(ethereum): pin no re-quote after a stop before a step replay
chybisov Oct 6, 2026
c5cbac9
docs(sdk): describe when a step replay is skipped
chybisov Oct 6, 2026
91c6463
fix(bitcoin): require @bigmi/core 0.9.3
chybisov Oct 6, 2026
3541fa3
fix(ethereum): check the action again before relaying a signed step
chybisov Oct 6, 2026
03d9ce6
fix(solana): check the action again after the wallet signs
chybisov Oct 6, 2026
8b4f005
fix(sui): check the action again after the wallet signs
chybisov Oct 6, 2026
29bc043
fix(tron): check the action again after the wallet signs
chybisov Oct 6, 2026
93ec54b
fix(bitcoin): check the action again after the wallet signs
chybisov Oct 6, 2026
5659ecf
fix(stellar): check the action again after the wallet signs
chybisov Oct 6, 2026
e82d753
fix(stellar): give each node a deadline in the absence proof
chybisov Oct 6, 2026
ab22281
docs: correct the stop order comment and the resume changesets
chybisov Oct 6, 2026
ad55873
docs: sharpen the resume changesets after the final review
chybisov Oct 6, 2026
0e0602f
Merge branch 'main' into feature/jumemb-79-tx-is-trying-to-be-resubmi…
chybisov Oct 7, 2026
f9d530d
test(tron): follow the static TRC-20 ABI in the flow specs
chybisov Oct 7, 2026
1248857
refactor(solana): remove the unused unwrapConfirmation
chybisov Oct 7, 2026
aa89b08
refactor(sui): remove the unreachable dropped branch after a refusal
chybisov Oct 7, 2026
85cc08b
docs(sdk): state the resume rules for providers in transactionState
chybisov Oct 7, 2026
f71af51
docs: state the resume rules in place of design-doc references
chybisov Oct 7, 2026
01b822b
test: state the resume rules in place of design-doc references
chybisov Oct 7, 2026
9573824
test(ethereum): stub the Tenderly lookup in the reload spec
chybisov Oct 7, 2026
fc04c15
refactor(sdk): stop exporting two helpers that no provider uses
chybisov Oct 7, 2026
a56064b
docs(sdk): document updateRouteHook and split the sdk changeset
chybisov Oct 7, 2026
e0aaeba
docs(sdk): explain why updateStepInRoute copies the step shallowly
chybisov Oct 7, 2026
7cd103a
test(sdk): check action return types without calling the actions
chybisov Oct 7, 2026
591beb7
fix(sdk): report errors of late transaction writes in development
chybisov Oct 7, 2026
f9718d2
docs(sdk): name the relayer verdict in the final-error rule
chybisov Oct 7, 2026
fe323aa
docs(sdk): make the resume rules true for every lane
chybisov Oct 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bump-bigmi-core.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lifi/sdk-provider-bitcoin": patch
---

Require `@bigmi/core` 0.9.3, which fixes `waitForTransaction`. A wait whose block budget ran out no longer stops the shared block watcher, so a resumed Bitcoin route, and every later wait on the same client, can no longer wait forever. Finished waits release their observers and timers. A transaction that a lagging node still reports as unconfirmed is no longer reported as its own replacement, and a replacement is always compared with the awaited transaction, so a fee bump of a cancel is still reported as cancelled.
5 changes: 5 additions & 0 deletions .changeset/resume-without-resign-bitcoin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lifi/sdk-provider-bitcoin": patch
---

Mark a cancelled replacement as final so "Try again" signs a new transaction; any other failure after broadcast is re-checked instead. The sign task refuses to sign while the step has an open transaction. It checks the action again after the wallet returns: if a stopped run's transaction merged into it while the prompt was open, the step fails with `TransactionConflict` and the new signature is neither stored nor sent. The signed transaction (`txHex`, `txHash`, `signedAt`) is now stored before it is sent, so a reload during the send resumes it instead of signing again. A first send that every configured node refuses for a reason that proves none of them holds it (bytes that do not decode, consensus or standardness rules, a fee above the cap or below the floor, or a full mempool), and that no node knows by txid, clears it, so "Try again" signs anew; any other send failure is re-checked, and the stored transaction is resent on a resume (a reload or "Try again") only within two minutes of signing. If the first send's outcome stays unknown and no resend happens within two minutes of signing, a resume only waits; the wait ends after about ten blocks with an error that is not final, and the exit is to delete the route.
5 changes: 5 additions & 0 deletions .changeset/resume-without-resign-ethereum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lifi/sdk-provider-ethereum": patch
---

Mark reverted, cancelled, replaced and relayed-failed transactions as final so "Try again" signs a new transaction, while any other failure after broadcast is re-checked instead. The sign task refuses to sign while the step has an open transaction. The relayed sign task checks the action again after the wallet returns: if a stopped run's transaction merged into it while the prompt was open, the step fails with `TransactionConflict` and the new signature is not relayed. The wait for a relayed transaction now ends after 24 hours (before, a task that stayed PENDING was polled every 5 seconds forever) and at once after `stopRouteExecution`; neither end is a final outcome, so the action keeps its task id and "Try again" waits for the same task instead of signing again. After `stopRouteExecution`, the wait ends without a write to the route, and the run's `executeRoute` promise resolves with the route, as for the `/status` wait. A resume waits on the lane that sent the open transaction, as stored in the action's `txType`, so a relayed step without a `transactionRequest` waits for its relayer task again, not for a transaction receipt or a wallet call bundle. A Safe wallet is not covered: the hash is stored only after the Safe transaction executes, so a resume during that wait, or after its 24-hour timeout, still creates a second proposal (unchanged).
21 changes: 21 additions & 0 deletions .changeset/resume-without-resign-sdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@lifi/sdk": minor
---

Never sign a second transaction on resume while the first one may still land.

- `prepareRestart` keeps a FAILED action with an unknown outcome and re-checks it on chain. Only a final outcome is dropped, so "Try again" signs anew.
- After `stopRouteExecution`, `updateRouteHook` can still be called for a route whose task was running, but only for a write that changes `txHash`, `txHex`, `taskId` or `txFinal`. An integrator that deleted the route should ignore that call. An error the hook throws in such a late call does not reach the run; in development it is logged with `console.debug`. If a new execution of the same route is running, a late transaction that may still land is merged into its action instead, so it does not sign again unless its wallet call has already started. If a newer execution already ended, the late transaction is merged into the route of the last ended execution, and that execution's `updateRouteHook` is called.
- The new `ExecutionAction.txFinal` flags a final outcome.
- `TransactionError` takes a new `final` option. Every `TransactionError` now has an own `final` property (`false` unless set), which changes its JSON shape.
- `resumeRoute` no longer mutates the route it is given. A restart keeps `execution.signedAt` while a transaction is open.
- In the EVM relayed lane and in the Solana, Sui, Tron, Bitcoin and Stellar providers, the SDK sends the transaction and checks the action again after the wallet returns. A prompt that was already open can still be signed, but when the late transaction merged first, those bytes are never sent or stored. A caught merge makes the newer run fail with `TransactionConflict`, and "Try again" then resumes and waits for the transaction. A step that asks for a replay (`ExecuteStepRetryError`) after such a merge also fails with `TransactionConflict`.
- An older run no longer stops or ends a newer execution of the same route.
- After `stopRouteExecution`, a run that waits for `/status` stops polling. Before, it polled every few seconds until the API answered DONE, and forever on FAILED, INVALID, NOT_FOUND or HTTP/network errors. The wait ends at once without a write to the route, so the step stays PENDING, and the run's `executeRoute` promise resolves with the route. A resume waits for the outcome of the same transaction.
- A stop during the first attempt of a step that then asks for a replay (`ExecuteStepRetryError`) also ends the run. The step does not run again, so the replay's re-quote, chain reads and integrator callbacks do not run after the stop, and `executeRoute` resolves with the route.
- `waitForResult` and `StepExecutor.executeStep` take an optional `signal`. Provider authors get it as `StepExecutorBaseContext.signal`, which aborts on `stopRouteExecution`. Give it only to a wait that starts after the transaction is broadcast, never to a sign, send or wallet call.
- New `hasOpenTransaction`. `assertNoOpenTransaction`, `isKnownToStatusApi`, `isFinalTransactionError`, `isResendAllowed`, `isOldEnoughToDrop`, `CLEARED_TRANSACTION_FIELDS`, `MAX_RESEND_AGE_MS`, `DROPPED_FALLBACK_AGE_MS` and `CLOCK_SKEW_MARGIN_MS` are exported for provider packages; they are not part of the integrator API (`@internal`).
- Remaining limits:
- In the lanes where the SDK sends, if the newer prompt is approved before the older run stores its transaction (in the EVM relayed lane, before the older run's relay request returns), nothing is merged yet, and both transactions can be sent.
- In the EVM standard and batched lanes the wallet sends, so a wallet call that has already started can still send a second transaction.
- A FAILED route from before the upgrade, or one resumed after node history no longer covers its signing time, can stay unknown. The exit is to delete the route.
5 changes: 5 additions & 0 deletions .changeset/resume-without-resign-solana.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lifi/sdk-provider-solana": patch
---

Resume a signed or broadcast transaction instead of signing again. The signed transaction is stored in `txHex` and resent after a reload until it confirms or its blockhash expires (a durable-nonce transaction only within two minutes of signing); a transaction is declared dropped only when it can no longer land, an RPC whose history covers the signing time does not find it, and the LI.FI status API does not know it. It can no longer land when its blockhash expired or, without a blockhash verdict (an RPC without `isBlockhashValid`, a probe error, no RPC answering, a route stored without `txHex`, or a durable-nonce transaction past its two-minute resend window), about 5 minutes after signing (`isOldEnoughToDrop`). The sign task checks the action again after the wallet returns: if a stopped run's transaction merged into it while the prompt was open, the step fails with `TransactionConflict` and the new signature is neither stored nor sent. The wallet account is resolved only when signing. An expired blockhash after the SDK stopped waiting now reports `TransactionExpired` instead of `RpcUnavailable` when an RPC answered.
5 changes: 5 additions & 0 deletions .changeset/resume-without-resign-stellar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lifi/sdk-provider-stellar": patch
---

Mark a FAILED result, and a rejected submission that the network confirms it never applied, as final so "Try again" signs a new transaction; any other failure after signing is re-checked instead. The check that the network never applied a rejected submission gives each RPC 10 seconds to answer; a node that does not answer in time gives no information, as a failed request does, so a hung node cannot hold this check. The sign task refuses to sign while the step has an open transaction. It checks the action again after the wallet returns: if a stopped run's transaction merged into it while the prompt was open, the step fails with `TransactionConflict` and the new signature is neither stored nor submitted.
5 changes: 5 additions & 0 deletions .changeset/resume-without-resign-sui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lifi/sdk-provider-sui": patch
---

Resume a signed or executed transaction instead of signing again. Signing and execution are split (same `signer.signTransaction` call), the signed bytes are stored in `txHex` before execution, and the digest is stored right after it, also for a failed execution. The sign task checks the action again after the wallet returns: if a stopped run's transaction merged into it while the prompt was open, the step fails with `TransactionConflict` and the new signature is neither stored nor executed. Stored bytes are re-executed only within two minutes of signing. After a node refuses the first execution, "Try again" cannot sign a new transaction until about 17 minutes after signing (the two-minute resend window, a 10-minute clock skew margin and a 5-minute head margin): no node can prove the transaction absent before then. The digest lookup and the absence-proof calls of a resume give each node 10 seconds to answer, so a hung node cannot hold the route: a node that does not answer in time counts as failed and the next node is asked; when no node answers, the outcome stays unknown. The final wait keeps its 60-second limit per node. A stored zkLogin signature cannot be verified offline, so such a route is never resent or declared dropped and stays unknown (the user deletes the route). A failed execution now reports the chain's error message instead of `[object Object]`. A wallet rejection is recognized at the signing call; a node message that contains "reject" is no longer reported as `SignatureRejected`, and an SDK error keeps its own code.
5 changes: 5 additions & 0 deletions .changeset/resume-without-resign-tron.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lifi/sdk-provider-tron": patch
---

Resume a signed or broadcast transaction instead of signing again. The sign task checks the action again after the wallet returns: if a stopped run's transaction merged into it while the prompt was open, the step fails with `TransactionConflict` and the new signature is neither stored nor broadcast. The signed transaction is stored in `txHex` and rebroadcast after a reload; it is declared dropped only after `raw_data.expiration` (by block time), when the node does not find it within the lookup window and the LI.FI status API does not know it. A route stored without `txHex` has no `raw_data.expiration`; its transaction counts as expired about 5 minutes after `signedAt` (`isOldEnoughToDrop`, on the device clock), and it can be declared dropped only once the chain's block time is past `signedAt` plus the clock-skew and head margins, about 16 minutes after signing. A confirmation timeout is no longer treated as a final failure.
2 changes: 1 addition & 1 deletion packages/sdk-provider-bitcoin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"watch": "tsdown --watch"
},
"dependencies": {
"@bigmi/core": "^0.9.2",
"@bigmi/core": "^0.9.3",
"@bitcoinerlab/secp256k1": "^2.0.0",
"@lifi/sdk": "workspace:*",
"bech32": "^2.0.0",
Expand Down
93 changes: 93 additions & 0 deletions packages/sdk-provider-bitcoin/src/client/publicClient.unit.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
AllTransportsFailedError,
HttpRequestError,
RpcRequestError,
} from '@bigmi/core'
import { ChainId, type SDKClient } from '@lifi/sdk'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { DECODE_FAILED } from '../core/tasks/bitcoinRpcErrors.unit.mock.js'
import { classifyBitcoinSendFailure } from '../core/tasks/classifyBitcoinSendFailure.js'
import { getBitcoinPublicClient } from './publicClient.js'

const URL_A = 'https://btc-a.example/'
const URL_B = 'https://btc-b.example/'
const TX_HEX = '0200000000010100'

/** Only what `getBitcoinPublicClient` reads from the SDK client. */
const sdkClient = {
getRpcUrlsByChainId: async () => [URL_A, URL_B],
getChainById: async () => ({
id: ChainId.BTC,
metamask: {
chainId: String(ChainId.BTC),
chainName: 'Bitcoin',
rpcUrls: [URL_A, URL_B],
blockExplorerUrls: ['https://mempool.space/'],
nativeCurrency: { name: 'Bitcoin', symbol: 'BTC', decimals: 8 },
},
}),
} as unknown as SDKClient

/**
* Every URL refuses the bytes. URL A answers as a JSON-RPC 2.0 node (HTTP
* 200 with an `error` member), URL B as a legacy node (HTTP 500 with the
* same body). A new `Response` per call: a body can be read only once.
*/
const stubRefusingNodes = (): ReturnType<typeof vi.fn> => {
const fetchMock = vi.fn(async (url: string | URL, init?: RequestInit) => {
const { id } = JSON.parse(String(init?.body)) as { id: number }
return new Response(
JSON.stringify({
jsonrpc: '2.0',
id,
result: null,
error: DECODE_FAILED,
}),
{
status: String(url) === URL_A ? 200 : 500,
headers: { 'Content-Type': 'application/json' },
}
)
})
vi.stubGlobal('fetch', fetchMock)
return fetchMock
}

const fetchesTo = (fetchMock: ReturnType<typeof vi.fn>): string[] =>
fetchMock.mock.calls.map(([url]) => String(url))

afterEach(() => {
vi.unstubAllGlobals()
})

describe('getBitcoinPublicClient sendrawtransaction', () => {
// The sign task's one-round proof rests on bigmi internals: the fallback
// gives each URL `retryCount: 0`, and the override stops the retry of the
// whole round. A bigmi release that changes either breaks this test.
it('sends one round with one fetch per URL and keeps one error per URL', async () => {
const fetchMock = stubRefusingNodes()
const client = await getBitcoinPublicClient(sdkClient, ChainId.BTC)

// The same call as BitcoinSignAndExecuteTask.
const thrown = await client
.request(
{ method: 'sendrawtransaction', params: [TX_HEX] },
{ retryCount: 0 }
)
.catch((error: unknown) => error)

expect(fetchesTo(fetchMock)).toEqual([URL_A, URL_B])
expect(thrown).toBeInstanceOf(AllTransportsFailedError)
// One entry per URL, in URL order: A's JSON-RPC error, B's HTTP 500.
expect(
(thrown as AllTransportsFailedError).errors.map(({ error }) =>
error instanceof RpcRequestError
? 'rpc'
: error instanceof HttpRequestError
? `http ${error.status}`
: 'other'
)
).toEqual(['rpc', 'http 500'])
expect(classifyBitcoinSendFailure(thrown)).toBe('refused')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
BaseStepExecutor,
CheckBalanceTask,
type ExecutionAction,
hasOpenTransaction,
LiFiErrorCode,
type LiFiStepExtended,
PrepareTransactionTask,
Expand Down Expand Up @@ -61,7 +62,9 @@ export class BitcoinStepExecutor extends BaseStepExecutor {
isBridgeExecution ? 'CROSS_CHAIN' : 'SWAP'
)

const firstTask = swapOrBridgeAction?.txHash
// Same predicate as the pre-sign guard: an open transaction is waited
// for, a FAILED action with a final outcome signs again.
const firstTask = hasOpenTransaction(swapOrBridgeAction)
? swapOrBridgeAction?.status === 'DONE'
? WaitForTransactionStatusTask
: BitcoinWaitForTransactionTask
Expand Down
Loading
Loading