Skip to content

feat: add Arbitrum USDT wallet with USDT fees - #157

Open
ben-kaufman wants to merge 11 commits into
masterfrom
feat/usdt-arbitrum
Open

ben-kaufman wants to merge 11 commits into
masterfrom
feat/usdt-arbitrum

Conversation

@ben-kaufman

@ben-kaufman ben-kaufman commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Adds USDT0 support on Arbitrum One to Bitkit Core, allowing users to receive and send USDT and pay transaction fees in USDT without holding ETH.

  • Derives an EIP-7702 account from the wallet's existing mnemonic and optional passphrase.
  • Provides balance, receive-address/payment-request, fee-quote, signing and activity APIs through UniFFI for the native apps.
  • Quotes a maximum USDT fee and validates the approved payment before signing. Signed operations are saved before submission so an interrupted or uncertain response does not lose the payment.
  • Recovers pending payments after restart and reconstructs incoming and outgoing activity from chain history after seed restoration. Activity includes payment status and fees where they can be attributed.

This PR covers direct Arbitrum payments. Native UI and the backend that protects provider credentials are maintained separately. Outbound bridging is added in #158; inbound deposits from other networks are added in #159.

QA Notes

  • Run cargo test --locked --lib modules::usdt, cargo fmt --check and cargo clippy --locked --lib --tests.
  • Coverage includes address/signature vectors, payment-request parsing, fee limits, uncertain submission, nonce recovery and restored history. An opt-in Arbitrum fork test exercises deployed contracts and USDT fee collection with zero account ETH.
  • Native-app mainnet testing has exercised send/receive, USDT-paid fees, and restart/seed recovery. Deployment and physical-device release validation remain separate requirements.
  • Chain history and payment outcomes depend on the configured RPC provider. A receipt at the current Arbitrum tip establishes L2 execution, not Ethereum finality.
  • Release packaging targets unpublished v0.6.0. Before remote package consumption, build the selected merged source, record the final SwiftPM checksum in a release-preparation commit, and tag/publish that exact iOS archive and matching Android package. Use matching local artifacts for branch testing; later stack layers must use a new version if an earlier layer has already been released.

@ben-kaufman ben-kaufman changed the title feat: add USDT0 wallet support on Arbitrum feat: add Arbitrum USDT wallet with USDT fees Sep 24, 2026
@ben-kaufman
ben-kaufman marked this pull request as ready for review September 25, 2026 12:45
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T13:16:03.158749Z 1f805f1 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1f805f1091

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +370 to +374
if let Ok(call) = Erc20::transferCall::abi_decode(&data) {
if payment.is_some() {
return None;
}
payment = Some((call.recipient, token_amount(call.amount).ok()?));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject zero-value and self-transfer restored payments

When restoring a supported operation created by another wallet, decode_payment accepts any decoded transfer, including a zero amount or a recipient equal to the wallet address. The raw-log path explicitly discards those transfers, and locally quoted payments prohibit them, but this calldata enrichment path reintroduces them as confirmed outgoing activity, so restoring the same seed can display a spurious payment (and collapse its other raw debits into that entry). Apply the same zero/self-transfer checks before returning the decoded payment.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ccdd478 and propagated to #158 and #159. Restore now excludes zero-value and self-directed ERC-20 transfers from decoded payments, preserving the raw fee debits and refunds.

I extended the existing raw-movement restoration test with both cases. It failed before the fix and now passes. All three USDT suites pass locally (48/54/60 tests), along with formatting, clippy and the iOS artifact checks. No signing, storage-schema or public API changes.

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Advice: ✅ Approve


Review: diff 33 files.

Findings:
1 inline (non-blocking)

Audit:
Audited - no findings.

Coverage:
QA: the tests under "QA Notes" need a person; no approval follows from this review


Reviewed by gpt-6-sol-xhigh via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest · audit (author or owner) · wrong <why> (owner)

Comment thread src/modules/usdt/wallet.rs
Comment thread src/modules/usdt/wallet.rs Outdated
.await?;
self.paymaster.validate_gas(&data.plan.operation).await?;
if self
.block_timestamp(self.block_number().await?)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recovery floor still comes from quote time. If the L2 head moves backward before send, this operation can execute below created_block. Recovery then misses the event, marks the delivered payment Replaced, clears the pending plan, and can allow a duplicate payment. Could we lower the stored recovery floor using the send-time head with a reorg margin, or expire the quote when the head moves backward? A regression for quote at N and execution below N would cover this.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d946fc2. Before signing and persisting, send() lowers the recovery floor using the current head and the existing 4096-block revisit margin. The test quotes at a higher head, sends after a retreat, then restarts and recovers the executed payment through both indexed logs and the nonce/receipt fallback. Reusing the quote ID returns the confirmed payment.

if address == TOKEN {
if let Ok(payment) = Erc20::Transfer::decode_log_data(&data) {
transfer_proven |= payment.from == self.address
&& payment.to == parse_address(&transfer.recipient)?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A restored failed operation can contain a zero-address attempted recipient. Processing its paymaster fee transfer calls parse_address on that recipient and returns InvalidAddress, so history progress is never saved. Could we treat chain-decoded recipients as raw addresses here, or skip recipient proof when the operation failed, and add a regression?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d946fc2. Recipient transfer proof is now checked only for successful execution. Failed operations still retain their attempted recipient, amount and actual paymaster fee. The restore test covers a failed zero-address payment with a token fee debit and verifies that repeated history scans complete without duplicates.

width = MAX_LOG_RANGE;
}
Err(UsdtError::NetworkUnavailable) => {
self.history_range_limit

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A transient network error permanently lowers the range ceiling for the initial history scan. The next call uses the reduced value as both width and ceiling, so successful requests cannot recover until the full scan finishes. Repeated failures can reduce this to one block and make restoration impractical. Could we keep temporary network backoff separate from the learned provider ceiling, or grow it again after successful requests?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d946fc2. Each scan resumes with the last request width but allows successful pages to grow again. Explicit range-limit responses still narrow the ceiling for that scan. The test verifies recovery to full query width while the initial history scan is still incomplete. No extra stored state was needed.

Comment thread src/modules/usdt/payment_request.rs Outdated
.unwrap_or(target)
.split_once('@')
.ok_or(UsdtError::WrongNetwork)?;
let Some(chain) = chain.strip_suffix("/transfer") else {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under ERC-681, an ethereum:@42161 URI without a function name requests native ETH, but this branch accepts it as USDT. Could we require scheme-qualified requests to target the pinned token transfer function and keep plain-address support separate?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d946fc2. Scheme-qualified requests must target the pinned USDT token and its transfer function. Native-asset URIs, including the pay- form, are rejected. Bare receiving addresses remain supported, and the tests cover both behaviors.

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Advice: ✅ Approve


Reaudit: diff 5 files.
No new findings; the rest is in the review.


Reviewed by gpt-6-sol-xhigh via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest · audit (author or owner) · wrong <why> (owner)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants