Skip to content

Commit 53c87ba

Browse files
committed
feat: refactor errors
1 parent 3391d3a commit 53c87ba

5 files changed

Lines changed: 145 additions & 235 deletions

File tree

go/mechanisms/evm/batched/client/extensions.go

Lines changed: 55 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,26 @@ func (c *BatchedEvmScheme) CreatePaymentPayloadWithExtensions(
6464
}
6565

6666
// Only Permit2 deposits gas-sponsor through these extensions; ERC-3009
67-
// deposits authorize the transfer in the same signature.
68-
if !isPermit2Deposit(requirements, result) {
67+
// deposits authorize the transfer in the same signature. Read
68+
// `assetTransferMethod` from requirements.Extra (TS source of truth)
69+
// and fall back to inspecting the deposit authorization shape so a
70+
// missing wire field still routes correctly.
71+
isPermit2 := false
72+
if requirements.Extra != nil {
73+
if v, ok := requirements.Extra["assetTransferMethod"].(string); ok && v != "" {
74+
isPermit2 = v == "permit2"
75+
}
76+
}
77+
if !isPermit2 && result.Payload != nil {
78+
if dep, ok := result.Payload["deposit"].(map[string]interface{}); ok {
79+
if auth, ok := dep["authorization"].(map[string]interface{}); ok {
80+
if _, has := auth["permit2Authorization"]; has {
81+
isPermit2 = true
82+
}
83+
}
84+
}
85+
}
86+
if !isPermit2 {
6987
return result, nil
7088
}
7189

@@ -81,28 +99,6 @@ func (c *BatchedEvmScheme) CreatePaymentPayloadWithExtensions(
8199
return result, nil
82100
}
83101

84-
// isPermit2Deposit reports whether the resolved deposit payload uses the
85-
// Permit2 transfer method. Reads requirements.Extra first (TS source of
86-
// truth) and falls back to inspecting the deposit authorization shape, so a
87-
// missing `assetTransferMethod` field on the wire still routes correctly.
88-
func isPermit2Deposit(requirements types.PaymentRequirements, payload types.PaymentPayload) bool {
89-
if requirements.Extra != nil {
90-
if v, ok := requirements.Extra["assetTransferMethod"].(string); ok && v != "" {
91-
return v == "permit2"
92-
}
93-
}
94-
if payload.Payload != nil {
95-
if dep, ok := payload.Payload["deposit"].(map[string]interface{}); ok {
96-
if auth, ok := dep["authorization"].(map[string]interface{}); ok {
97-
if _, has := auth["permit2Authorization"]; has {
98-
return true
99-
}
100-
}
101-
}
102-
}
103-
return false
104-
}
105-
106102
// trySignEip2612Permit attempts to sign an EIP-2612 permit authorizing Permit2
107103
// to spend the deposit amount. Returns nil (no error) when the extension is
108104
// not advertised, the token does not expose name/version, the signer cannot
@@ -147,8 +143,19 @@ func (c *BatchedEvmScheme) trySignEip2612Permit(
147143

148144
tokenAddress := evm.NormalizeAddress(requirements.Asset)
149145

150-
depositAmount, ok := readDepositAmount(result)
151-
if !ok {
146+
// Pull `payload.deposit.amount` from the freshly-built batched deposit
147+
// payload. The signed EIP-2612 `info.amount` must equal the BATCH
148+
// deposit (what the facilitator's `validateBatchEip2612Permit` checks),
149+
// not `requirements.Amount` (the per-request charge).
150+
var depositAmount string
151+
if result.Payload != nil {
152+
if dep, ok := result.Payload["deposit"].(map[string]interface{}); ok {
153+
if amt, ok := dep["amount"].(string); ok && amt != "" {
154+
depositAmount = amt
155+
}
156+
}
157+
}
158+
if depositAmount == "" {
152159
return nil, nil
153160
}
154161

@@ -159,7 +166,22 @@ func (c *BatchedEvmScheme) trySignEip2612Permit(
159166
return nil, nil
160167
}
161168

162-
deadline := readPermit2DeadlineFromBatchedDeposit(result)
169+
// Align EIP-2612 deadline with the just-signed Permit2 authorization
170+
// (`payload.deposit.authorization.permit2Authorization.deadline`) so
171+
// both signatures expire together. Falls back to
172+
// `now + maxTimeoutSeconds` when the field isn't present.
173+
deadline := ""
174+
if result.Payload != nil {
175+
if dep, ok := result.Payload["deposit"].(map[string]interface{}); ok {
176+
if auth, ok := dep["authorization"].(map[string]interface{}); ok {
177+
if permit2, ok := auth["permit2Authorization"].(map[string]interface{}); ok {
178+
if d, ok := permit2["deadline"].(string); ok {
179+
deadline = d
180+
}
181+
}
182+
}
183+
}
184+
}
163185
if deadline == "" {
164186
deadline = fmt.Sprintf("%d", time.Now().Unix()+int64(requirements.MaxTimeoutSeconds))
165187
}
@@ -214,14 +236,12 @@ func (c *BatchedEvmScheme) trySignErc20Approval(
214236
tokenAddress := evm.NormalizeAddress(requirements.Asset)
215237

216238
if readSigner, hasRead := c.signer.(evm.ClientEvmSignerWithReadContract); hasRead {
217-
// Use deposit amount, not requirements.Amount, since the deposit is
218-
// what the facilitator will pull through Permit2 — that's what needs
219-
// to fit under the existing allowance.
220-
needed := requirements.Amount
221-
if v, ok := readDepositAmountFromAnyPayload(c, requirements); ok {
222-
needed = v
223-
}
224-
if hasSufficientPermit2Allowance(ctx, readSigner, tokenAddress, c.signer.Address(), needed) {
239+
// Approximate the deposit amount with the per-request amount —
240+
// sufficient for the allowance short-circuit since `deposit ≥
241+
// requirements.Amount` always holds for batched flows. Skip the
242+
// short-circuit when no per-request amount is available.
243+
if requirements.Amount != "" &&
244+
hasSufficientPermit2Allowance(ctx, readSigner, tokenAddress, c.signer.Address(), requirements.Amount) {
225245
return nil, nil
226246
}
227247
}
@@ -238,59 +258,6 @@ func (c *BatchedEvmScheme) trySignErc20Approval(
238258
}, nil
239259
}
240260

241-
// readDepositAmount pulls `payload.deposit.amount` from a freshly built
242-
// batched deposit payload map. Returns ("", false) for non-deposit payloads.
243-
func readDepositAmount(payload types.PaymentPayload) (string, bool) {
244-
if payload.Payload == nil {
245-
return "", false
246-
}
247-
dep, ok := payload.Payload["deposit"].(map[string]interface{})
248-
if !ok {
249-
return "", false
250-
}
251-
amt, ok := dep["amount"].(string)
252-
if !ok || amt == "" {
253-
return "", false
254-
}
255-
return amt, true
256-
}
257-
258-
// readDepositAmountFromAnyPayload is a fallback for callers that don't have
259-
// the just-built payload in hand (e.g. `trySignErc20Approval` running before
260-
// the deposit payload has been re-resolved). Approximates with the per-request
261-
// amount — sufficient for the allowance short-circuit since deposit ≥ amount
262-
// always holds for batched flows.
263-
func readDepositAmountFromAnyPayload(_ *BatchedEvmScheme, requirements types.PaymentRequirements) (string, bool) {
264-
if requirements.Amount == "" {
265-
return "", false
266-
}
267-
return requirements.Amount, true
268-
}
269-
270-
// readPermit2DeadlineFromBatchedDeposit extracts the deadline from the batched
271-
// deposit's Permit2 authorization map: payload.deposit.authorization.permit2Authorization.deadline.
272-
// Mirrors the TS scheme which aligns the EIP-2612 deadline with the Permit2
273-
// deadline so both expire together.
274-
func readPermit2DeadlineFromBatchedDeposit(payload types.PaymentPayload) string {
275-
if payload.Payload == nil {
276-
return ""
277-
}
278-
dep, ok := payload.Payload["deposit"].(map[string]interface{})
279-
if !ok {
280-
return ""
281-
}
282-
auth, ok := dep["authorization"].(map[string]interface{})
283-
if !ok {
284-
return ""
285-
}
286-
permit2, ok := auth["permit2Authorization"].(map[string]interface{})
287-
if !ok {
288-
return ""
289-
}
290-
d, _ := permit2["deadline"].(string)
291-
return d
292-
}
293-
294261
// hasSufficientPermit2Allowance returns true when `owner` has already
295262
// approved at least `requiredAmount` to Permit2 for `tokenAddress`. Returns
296263
// false on any RPC error so we conservatively sign an extension rather than

go/mechanisms/evm/batched/client/extensions_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,15 @@ func TestCreatePaymentPayloadWithExtensions_Eip2612DeadlineFromPermit2(t *testin
290290
}
291291
wrapper := out.Extensions[eip2612gassponsor.EIP2612GasSponsoring.Key()].(map[string]interface{})
292292
info := wrapper["info"].(*eip2612gassponsor.Info)
293-
depDeadline := readPermit2DeadlineFromBatchedDeposit(out)
293+
// Read the deadline straight off the deposit payload — the alignment
294+
// invariant being tested is that whatever value lands in the Permit2
295+
// authorization (`payload.deposit.authorization.permit2Authorization.deadline`)
296+
// is reused for the EIP-2612 permit. Inlined here because the helper
297+
// was removed (single-call site) and the path is short.
298+
dep, _ := out.Payload["deposit"].(map[string]interface{})
299+
auth, _ := dep["authorization"].(map[string]interface{})
300+
permit2, _ := auth["permit2Authorization"].(map[string]interface{})
301+
depDeadline, _ := permit2["deadline"].(string)
294302
if depDeadline == "" {
295303
t.Fatal("permit2 deadline missing from deposit payload")
296304
}

go/mechanisms/evm/batched/facilitator/deposit.go

Lines changed: 21 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -214,16 +214,30 @@ func VerifyDeposit(
214214
collectorData,
215215
)
216216
if simErr != nil {
217-
invalidReason := ErrDepositSimulationFailed //nolint:ineffassign // overwritten on diagnosis
217+
// Diagnose the most common standard-Permit2-path simulation
218+
// revert: the user hasn't approved Permit2. We probe
219+
// `allowance(payer, Permit2)` and surface the dedicated reason
220+
// when it's below the deposit amount; any other revert (signature
221+
// invalidation, balance, etc.) passes through as the generic
222+
// ErrDepositSimulationFailed. Mirrors exact's
223+
// `CheckPermit2Prerequisites` diagnosis. RPC failures during the
224+
// probe also fall through to the generic reason.
225+
invalidReason := ErrDepositSimulationFailed
218226
if transferMethod == batched.AssetTransferMethodPermit2 &&
219227
(permit2Branch == nil || permit2Branch.kind == permit2BranchStandard) {
220-
if probedReason := diagnosePermit2AllowanceShortfall(ctx, signer, config, depositAmount); probedReason != "" {
221-
invalidReason = probedReason
222-
} else {
223-
invalidReason = ErrDepositSimulationFailed
228+
if allowanceResult, allowErr := signer.ReadContract(
229+
ctx,
230+
config.Token,
231+
evm.ERC20AllowanceABI,
232+
"allowance",
233+
common.HexToAddress(config.Payer),
234+
common.HexToAddress(evm.PERMIT2Address),
235+
); allowErr == nil {
236+
if allowance, ok := allowanceResult.(*big.Int); ok && allowance != nil &&
237+
allowance.Cmp(depositAmount) < 0 {
238+
invalidReason = ErrPermit2AllowanceRequired
239+
}
224240
}
225-
} else {
226-
invalidReason = ErrDepositSimulationFailed
227241
}
228242
return &x402.VerifyResponse{ //nolint:nilerr // simulation failure → error encoded in response
229243
IsValid: false,
@@ -691,40 +705,3 @@ func verifyPermit2DepositAuthorization(
691705
}
692706
return "", nil
693707
}
694-
695-
// diagnosePermit2AllowanceShortfall is called after a standard-path Permit2
696-
// deposit simulation reverts. It probes the on-chain ERC-20 allowance
697-
// payer→Permit2 and returns `ErrPermit2AllowanceRequired` when the allowance
698-
// is strictly less than the deposit amount (the canonical cause of the most
699-
// common standard-path simulation revert). On any RPC error or sufficient
700-
// allowance the helper returns "" so the caller falls back to the generic
701-
// `ErrDepositSimulationFailed` reason. Mirrors exact's
702-
// `CheckPermit2Prerequisites` diagnosis pattern but kept inline because the
703-
// batched scheme needs only this single check (other reverts pass through as
704-
// generic simulation failures).
705-
func diagnosePermit2AllowanceShortfall(
706-
ctx context.Context,
707-
signer evm.FacilitatorEvmSigner,
708-
config batched.ChannelConfig,
709-
depositAmount *big.Int,
710-
) string {
711-
allowanceResult, err := signer.ReadContract(
712-
ctx,
713-
config.Token,
714-
evm.ERC20AllowanceABI,
715-
"allowance",
716-
common.HexToAddress(config.Payer),
717-
common.HexToAddress(evm.PERMIT2Address),
718-
)
719-
if err != nil {
720-
return ""
721-
}
722-
allowance, ok := allowanceResult.(*big.Int)
723-
if !ok || allowance == nil {
724-
return ""
725-
}
726-
if allowance.Cmp(depositAmount) < 0 {
727-
return ErrPermit2AllowanceRequired
728-
}
729-
return ""
730-
}

0 commit comments

Comments
 (0)