Skip to content

feat: add --idempotency-key to transfer and swap, and tx status - #239

Open
brendanjryan wants to merge 3 commits into
mainfrom
brendan/idempotency-keys
Open

brendanjryan wants to merge 3 commits into
mainfrom
brendan/idempotency-keys

Conversation

@brendanjryan

@brendanjryan brendanjryan commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Agents retry transfer and swap after timeouts and lost responses, and today a retry sends the transaction again. When the CLI reports that a send may have gone through, there is also no command to check it.

Summary

  • --idempotency-key <key> on token transfer and swap. A submitting record is written before sending, then updated with the hash and result. Rerunning the key sends nothing and returns the recorded result, with status read from the chain and idempotent_replay: true.
  • Reusing a key with different normalized args (amount, token, recipient or tokens, fee token, mode) fails with E_USAGE naming the fields that differ.
  • If a key's send never recorded an outcome (a concurrent run, a crash, or an ambiguous failure), a rerun returns E_PAYMENT_OUTCOME_UNKNOWN and does not resend.
  • Replays and unknown outcomes need no usable access key. An agent run of a keyed send checks the key only when no record exists and it is about to sign.
  • transfer and swap now return E_PAYMENT_OUTCOME_UNKNOWN with a cta. When the hash is known, the message names it and the cta is tempo wallet tx status <hash> --network <n>. Otherwise the cta is tempo wallet whoami.
  • New tempo wallet tx status <hash> (MCP tx_status: read-only, idempotent, open-world). It reports success, reverted, pending or not_found, plus the block, from, to and fee. A pending result gets a polling _next with until. The command is wired into --describe and completions.
  • Dry runs never write records, and their _next carries the key.
  • README, SKILL.md (agents pass a key and reuse it on retries), and a changelog entry.

Key design considerations

  • Store: one JSON file per key, mode 0600, in ~/.tempo/wallet/idempotency/ (mode 0700). Each file is named by a sha256 of the wallet, chain ID and key. Per-key files avoid read-modify-write races between concurrent sends that use different keys.
  • The claim is an exclusive create (wx), so when two sends race with the same key, only one sends. The claim file and its directory are fsynced before the send, so the claim survives a host crash. The sqlite store was not used because it shells out to sqlite3, which transfers do not otherwise need.
  • Each keyed lookup first prunes records older than 30 days. Pruning and claims share a lock file, so a prune cannot delete a claim made after it read the expired record. The lock reuses the session lock's logic, extracted as withFileLock.
  • For keyed sends that are not dry runs, withWallet loads the wallet without the agent key check. The command returns keyRequiredError (now exported from require-wallet.ts) in place of sending under an unused key. Dry runs and sends without a key are checked up front as before.
  • A failure from submit keeps the claim, so a rerun never resends. The one exception is an error the CLI raises itself before signing, such as a wallet that is no longer logged in. That error frees the key.
  • A send error that carries an EIP-7966 timeout (code 4, with the hash as data) records the hash and returns an E_PAYMENT_OUTCOME_UNKNOWN that names it.
  • Replays run before --yes, quoting and balance checks. A replay sends nothing, and a swap that already completed may no longer pass those checks.
  • For transfers, the fee_token in the fingerprint is the --fee-token flag, or null when it is omitted, which matches what a retry of the same command passes.

Examples

$ tempo wallet transfer 5 USDC.e 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 --fee-token 0x20c000000000000000000000b9537d11c60e8b50 --idempotency-key order-1234 --yes --format json
{
  "status": "success",
  "tx_hash": "0x5e1f0c8a9d7b3e2f4a6c8e0b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f",
  "chain_id": 4217,
  "network": "tempo",
  "amount": "5",
  "symbol": "USDC.e",
  "token": "0x20c000000000000000000000b9537d11c60e8b50",
  "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8",
  "from": "0xabfb663c1f9cd7438f54846d6b827e315719ec0f",
  "idempotent_replay": true
}

$ tempo wallet transfer 6 USDC.e 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 --fee-token 0x20c000000000000000000000b9537d11c60e8b50 --idempotency-key order-1234 --yes --format json
{
  "code": "E_USAGE",
  "message": "Idempotency key \"order-1234\" was already used on this wallet and network for a different request (different amount). Use a new key for a new request.",
  "retryable": false
}

$ tempo wallet transfer 5 USDC.e 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 --fee-token 0x20c000000000000000000000b9537d11c60e8b50 --idempotency-key order-1235 --yes --format json
{
  "code": "E_PAYMENT_OUTCOME_UNKNOWN",
  "message": "Transfer with idempotency key \"order-1235\" was submitted, but its outcome was not recorded. Check transaction 0x3b7d9f1a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f5e1f0c8a9d with 'tempo wallet tx status 0x3b7d9f1a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f5e1f0c8a9d --network mainnet' before retrying.",
  "retryable": false,
  "cta": {
    "description": "Suggested command:",
    "commands": [
      {
        "command": "tempo wallet tx status 0x3b7d9f1a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f5e1f0c8a9d --network mainnet"
      }
    ]
  }
}

$ tempo wallet tx status 0x3b7d9f1a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f5e1f0c8a9d --format json
{
  "tx_hash": "0x3b7d9f1a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f5e1f0c8a9d",
  "chain_id": 4217,
  "network": "tempo",
  "status": "pending",
  "block_number": null,
  "from": "0xabfb663c1f9cd7438f54846d6b827e315719ec0f",
  "to": "0x20c000000000000000000000b9537d11c60e8b50",
  "fee": null,
  "fee_token": null,
  "instruction": "The transaction is pending. Rerun the next command until its status is no longer pending.",
  "_next": {
    "argv": ["tempo", "wallet", "tx", "status", "0x3b7d9f1a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f5e1f0c8a9d", "--network", "mainnet"],
    "command": "tempo wallet tx status 0x3b7d9f1a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f5e1f0c8a9d --network mainnet",
    "until": ".status != \"pending\"",
    "poll_interval_seconds": 1
  }
}

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 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-10-15T18:48:26.980961Z d5f2c60 PR opened
ℹ️ 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.

@tempoxyz-bot

tempoxyz-bot commented Oct 15, 2026 •

Copy link
Copy Markdown

✅ Cyclops Security Review: safe to merge

Last reviewed: ec211c8 (latest) · no open findings

# Severity Finding Introduced Status
1 ⚪ Informational Concurrent pruning can delete a newly claimed idempotency record d5f2c60 ✅ Fixed in 638999f
2 ⚪ Informational Idempotency claim not flushed to disk before transaction broadcast d5f2c60 ✅ Fixed in 638999f

@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: d5f2c60736

ℹ️ 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 thread src/commands/swap.ts
Comment thread src/wallet/idempotency.ts Outdated
Comment thread src/wallet/idempotency.ts
@brendanjryan
brendanjryan force-pushed the brendan/idempotency-keys branch from 638999f to f9f6803 Compare October 15, 2026 20:24
@brendanjryan
brendanjryan force-pushed the brendan/idempotency-keys branch from f9f6803 to ec211c8 Compare October 15, 2026 20:33

This branch has not been deployed

No deployments
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