Workspace IndexKnowledge Notes › The double-click mints twice — idempotency is the app's duty

#142PoC

The double-click mints twice — idempotency is the app's duty

The chain dedupes nonces, not intents: a user double-clicking Buy produces two valid transactions, and both settle. Payment APIs solved this decades ago with idempotency keys — on-chain apps have to rebuild that discipline themselves.

Build a checkout that issues an intent ID per user action, dedupes on it client-side and server-side, and a control version without it. Double-click both, slow the RPC to widen the race, and count what settles.

Why

Every payment API since Stripe ships idempotency keys because retries and double-clicks are how real users behave. On-chain the problem is worse: the wallet happily signs twice, both transactions carry different nonces so the chain sees two distinct valid payments, and finality means no one un-charges the second one.

The fix cannot live on the chain — the chain has no concept of the user's intent. It lives in the application: one intent, one ID, and every layer (button state, server, submission queue) refuses to act on an ID it has seen. This is the smallest possible example of a larger truth: application-level guarantees have to be built at the application level, even when the settlement layer is perfect.

How it works

One checkout, two builds — with and without intent IDs — and a race you widen on purpose.

PoC

A mint-or-buy button against anvil. Version A: on click, create intent { id, action, params }, disable the button on pending id, have the server (or client queue) refuse a second submission with the same id, and store the mapping id → txHash so a retry returns the existing receipt instead of a new transaction. Version B: none of that. Double-click both under a artificially slow RPC; assert version A settles exactly one transaction and version B settles two.

What it proves

Deduplication by intent is an application responsibility, and it is cheap: one UUID, one lookup table, one disabled button. The chain's own uniqueness (nonces) protects against replay of the same signed bytes — it does nothing against the same human wish signed twice.

Where it lands in Jayverse

  • Verex: give every Buy action an intent ID, deduped client- and server-side. Build the control-vs-fixed test the PoC describes — double-click under an artificially slow RPC and assert the fixed version settles exactly one order.
  • Token: apply the same intent-ID dedupe to the JYVE mint/AMM front end. A double-click on mint is the same bug with a worse name; store id → txHash so a retry returns the existing receipt.
  • Wallet: disable the action button on a pending intent ID. This is the cheapest layer of the fix — one UUID, one lookup table, one disabled button — and it belongs in Wallet's shared checkout component, not per-service.

Key expressions

Words and phrases from this page worth keeping, with the Korean meaning and the sentence they come from.

Expression뜻 · 쓰이는 자리
idempotency key멱등성 키(같은 요청을 여러 번 보내도 한 번만 처리되게 하는 식별자) · 결제 API의 중복 방지 표준 기법. "Payment APIs solved this decades ago with idempotency keys"
un-charge청구를 취소하다·환불 처리하다 · 이미 확정된 결제를 되돌릴 방법이 없을 때. "finality means no one un-charges the second one"
cannot live on~에만 맡겨둘 수 없다·~차원에서 해결될 수 없다 · 해결책이 다른 계층에 있어야 할 때. "The fix cannot live on the chain"
refuse to act on~에 대해 처리를 거부하다 · 이미 본 ID에 대해 재처리를 막을 때. "refuses to act on an ID it has seen"
widen on purpose일부러 (경합 조건을) 크게 벌리다 · 버그를 재현하려고 레이스 컨디션을 의도적으로 키울 때. "and a race you widen on purpose"
settle (a transaction)거래가 확정·처리되다 · 몇 건의 트랜잭션이 실제로 체결됐는지 셀 때. "assert version A settles exactly one transaction"
smallest possible example of a larger truth더 큰 원리를 보여주는 가장 작은 사례 · 작은 버그가 일반 원칙을 드러낼 때. "the smallest possible example of a larger truth"
double-click (trigger duplicate action)두 번 눌러서 같은 동작이 중복 발생하는 것 · 사용자 실수로 두 개의 유효한 거래가 생길 때. "a user double-clicking Buy produces two valid transactions"

← All Knowledge Notes · Workspace Index · Top ↑

더블클릭은 두 번 민트한다 — 멱등성은 앱의 의무

체인은 논스를 중복 제거하지 의도는 못 합니다: Buy 를 더블클릭한 사용자는 유효한 트랜잭션 둘을 만들고, 둘 다 체결됩니다. 결제 API 들은 수십 년 전 idempotency key 로 해결한 문제 — 온체인 앱은 그 규율을 직접 다시 만들어야 합니다.

사용자 액션마다 intent ID 를 발급하고 클라이언트·서버 양쪽에서 그것으로 중복 제거하는 체크아웃과, 그것이 없는 대조군을 만듭니다. 둘 다 더블클릭하고, RPC 를 느리게 해 경쟁 구간을 넓힌 뒤, 무엇이 체결되는지 셉니다.

Stripe 이후의 모든 결제 API 가 idempotency key 를 싣는 이유는, 재시도와 더블클릭이 실제 사용자의 행동이기 때문입니다. 온체인에서는 문제가 더 나쁩니다: 지갑은 기꺼이 두 번 서명하고, 두 트랜잭션은 논스가 달라 체인에게는 서로 다른 유효한 결제 둘이며, 파이널리티 때문에 두 번째를 취소해 줄 사람이 없습니다.

해법은 체인에 있을 수 없습니다 — 체인에는 사용자 의도라는 개념이 없으니까요. 해법은 애플리케이션에 삽니다: 의도 하나, ID 하나, 그리고 모든 계층(버튼 상태, 서버, 제출 큐)이 이미 본 ID 에는 행동을 거부하는 것. 이것은 더 큰 진실의 가장 작은 예입니다: 애플리케이션 레벨의 보장은 정산 계층이 완벽해도 애플리케이션 레벨에서 만들어야 합니다.

동작 방식

체크아웃 하나, 빌드 둘 — intent ID 가 있는 버전과 없는 버전 — 그리고 일부러 넓힌 경쟁 구간.

PoC

anvil 을 겨냥한 민트/구매 버튼. 버전 A: 클릭 시 intent { id, action, params } 생성, 대기 중인 id 에는 버튼 비활성화, 서버(또는 클라이언트 큐)는 같은 id 의 두 번째 제출을 거부, id → txHash 매핑을 저장해 재시도에는 새 트랜잭션 대신 기존 영수증을 반환. 버전 B: 그것 전부 없음. 인위적으로 느린 RPC 아래에서 둘 다 더블클릭하고, A 는 정확히 하나, B 는 둘이 체결됨을 확인합니다.

무엇을 증명하나

의도 기준 중복 제거는 애플리케이션의 책임이고, 쌉니다: UUID 하나, 조회 테이블 하나, 비활성화된 버튼 하나. 체인 자체의 유일성(논스)은 같은 서명 바이트의 재사용을 막을 뿐 — 같은 인간의 소망이 두 번 서명되는 것은 전혀 막지 못합니다.

Jayverse에서의 위치

  • Verex: 모든 Buy 액션에 클라이언트·서버 양쪽에서 중복 제거되는 의도 ID를 부여한다. PoC가 설명하는 대조군 테스트를 만든다 — 인위적으로 느린 RPC에서 더블클릭하고, 고정된 버전이 정확히 하나의 주문만 정산하는지 검증한다.
  • Token: JYVE 민트/AMM 프론트엔드에도 같은 의도 ID 중복 제거를 적용한다. 민트에서의 더블클릭은 같은 버그에 더 나쁜 이름이 붙은 것뿐이다. id → txHash를 저장해 재시도가 새 트랜잭션이 아니라 기존 영수증을 돌려주게 한다.
  • Wallet: 대기 중인 의도 ID가 있으면 액션 버튼을 비활성화한다. UUID 하나, 조회 테이블 하나, 비활성화된 버튼 하나로 끝나는 가장 저렴한 방어층이며, 서비스마다 따로가 아니라 Wallet의 공용 체크아웃 컴포넌트에 넣어야 한다.

핵심 표현

이 페이지의 영어 본문에서 배울 만한 단어와 표현, 뜻과 나온 자리.

Expression뜻 · 쓰이는 자리
idempotency key멱등성 키(같은 요청을 여러 번 보내도 한 번만 처리되게 하는 식별자) · 결제 API의 중복 방지 표준 기법. "Payment APIs solved this decades ago with idempotency keys"
un-charge청구를 취소하다·환불 처리하다 · 이미 확정된 결제를 되돌릴 방법이 없을 때. "finality means no one un-charges the second one"
cannot live on~에만 맡겨둘 수 없다·~차원에서 해결될 수 없다 · 해결책이 다른 계층에 있어야 할 때. "The fix cannot live on the chain"
refuse to act on~에 대해 처리를 거부하다 · 이미 본 ID에 대해 재처리를 막을 때. "refuses to act on an ID it has seen"
widen on purpose일부러 (경합 조건을) 크게 벌리다 · 버그를 재현하려고 레이스 컨디션을 의도적으로 키울 때. "and a race you widen on purpose"
settle (a transaction)거래가 확정·처리되다 · 몇 건의 트랜잭션이 실제로 체결됐는지 셀 때. "assert version A settles exactly one transaction"
smallest possible example of a larger truth더 큰 원리를 보여주는 가장 작은 사례 · 작은 버그가 일반 원칙을 드러낼 때. "the smallest possible example of a larger truth"
double-click (trigger duplicate action)두 번 눌러서 같은 동작이 중복 발생하는 것 · 사용자 실수로 두 개의 유효한 거래가 생길 때. "a user double-clicking Buy produces two valid transactions"

← 전체 기술 노트 · 워크스페이스 인덱스 · 맨 위 ↑