Skip to content

demo-95: reference-aware pay (payIn*WithReference), real permit allowlist (HP-5), 0.6.0 — supersedes #47 - #48

Merged
marceloceccon merged 4 commits into
developfrom
feat/demo-95-sdk-reference-sub-2026-09-29
Sep 30, 2026
Merged

marceloceccon merged 4 commits into
developfrom
feat/demo-95-sdk-reference-sub-2026-09-29

Conversation

@marceloceccon

@marceloceccon marceloceccon commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

demo-95 (Oct-7 demo) — W1 reference-aware pay + W2 green build. Supersedes #47 (its toolchain commit is carried verbatim as 58c1815). Part of the PR set on feat/demo-95-sdk-reference-sub-2026-09-29. Do not merge without Marcelo's go. Nothing published — note that merging to develop lets CI publish its usual 0.6.0-dev.<run> to GitHub Packages (HP-3).

Why

The browser could only send plain payInNative / payInToken, so every SDK payment was detected and settled by the gateway but attributed to no payment request: no status progression, no paymentRequestId / metadata on the webhook, nobody to credit. MerchantPayIn V3.2.3 (merged) has payIn*WithReference; this makes the SDK use it — by default.

What changes (commits)

Commit Change
58c1815 #47 unchanged: ESLint 9 pin, Tailwind v4 PostCSS plugin, esbuild — lint/build restored.
75f915b HP-5. KNOWN_PERMIT_TOKENS: the placeholder digest → the real digests for USDC Ethereum, DAI Ethereum, USDC Base, each re-derived from the chain (scripts/verify-permit-domains.mjs: name()/version() read, DOMAIN_SEPARATOR() rebuilt and matched — executed 2026-09-30; docs/PERMIT_ALLOWLIST.md). The four failing signPermit tests now target the allow-listed USDC domain and reach the guard each is named after; the unknown-domain refusal has its own test (wallet never asked). ADR-0004 not weakened, no fake domain. ⚠️ DAI's on-chain PERMIT_TYPEHASH is Permit(holder,spender,nonce,expiry,allowed) — not EIP-2612 (executed). Allow-listing it alone would make permit:'auto' sign a payload DAI cannot redeem and call a selector it lacks, breaking DAI payments that fall back to approve() today. So the digest is in (owner decision) and detectPermitSupport treats any token whose PERMIT_TYPEHASH isn't EIP-2612's as unsupported → approve(). Tokens without that view (OpenZeppelin) are unaffected.
ef56df7 W1. ABI + selectors (0x817da306, 0xa7d020be, pinned against the compiled V3.2.3 artifact); pure buildPayInCall; executePayIn{Native,Token}WithReference; resolveAttribution — the one policy every surface applies before the wallet is touched: attributed is the default and refuses without a valid non-zero bytes32 (MissingPaymentReferenceError); plain payIn* only on mode: 'unattributed'. usePayment().startPayment(…, { paymentReference }); Web3SettleConfig.createPayment(ctx) lets the modal get the reference from the merchant backend at Pay time (exact chain/token/amount + the userId the modal used to drop); headless start(amount, { paymentReference }) → runner; <web3settle-pay-button payment-reference>. Breaking: createTopUpSession / getSessionStatus (+ CreateSessionResponse(Schema), SESSION_POLL_INTERVAL_MS, MAX_POLL_ATTEMPTS) removed — they called /api/storefronts/{id}/sessions, which no gateway serves (404), and payment creation needs the API key, which must stay server-side.
66707f1 README leads with the real create → reference → payIn*WithReference → status → v1 webhook flow; peer table fixed to package.json; known limitation stated; CHANGELOG 0.6.0; version 0.6.0 (chain cut + removed API + new ABI are breaking on 0.x; one line to revert to 0.5.1 if you prefer — HP-3).

API shape chosen (work pack "prefer startPayment({ paymentReference }) OR createAndPay"): startPayment({ paymentReference }) + the modal's createPayment callback. One knob, mode: 'attributed' | 'unattributed' (default attributed) — requireReference: true is exactly the default; a second boolean for the same invariant was not added (R7). No createAndPay, no invented sessions route.

Acceptance — evidence tags

  • SDK attributes a pay-in via WithReference — executed end to end: the demo's browser click path pays through usePayment, and the E2E decodes the mined calldata as payInNativeWithReference(reference) (merchant-integration-demo e2e/sdk-bound-checkout.spec.ts, 19/19 on a wiped stack, three runs).
  • unit tests: WithReference selector/encoding, refusal without a reference before any wallet call (React hook, headless runner, WC) — executed.
  • fix: post-chain-cut remediation (CI / integrity) #47 superseded, build green — executed locally from clean trees: typecheck pass · lint 0 errors (the 10 pre-existing set-state-in-effect warnings) · vitest 199 passed, 0 failed (was 165 / 4 failing) · build pass. The demo Docker image ran this SDK's npm ci && npm run build successfully (executed).
  • TRON (./tron) pay-ins are not reference-aware yet (root humanpending.md HP-25).
  • Known limitation: WEB3SETTLE_PAYMENT_CONFIG_PUBKEY_PRIMARY is still all-zero, so Web3SettleTopUpModal / Web3SettlePayButton / the headless controller cannot load a real gateway's signed config (HP-24 — needs the real key). The demo therefore uses usePayment with its own chain config.

Commits authored as Marcelo Ceccon <marcelo@ceccon.org> (git identity set per repo in this devcontainer, from the repos' history) by the demo-95 agent run.

Root PR (runbook DEMO_OCT7.md, humanpending, SoftwareGaps): web3settle/web3settle-monorepo#7

Toolchain only. No public API change; version deliberately stays 0.5.0. Every
chain-cut guarantee intact: archive/solana still unwired, exports map unchanged,
137 / 900-902 drop guards still pass.

LINT — was crashing, now runs clean
eslint-plugin-react under ESLint 10 threw "contextOrFilename.getFilename is not a
function" before linting a single file. No published eslint-plugin-react (max
7.37.5, peers eslint ^3..^9.7) or eslint-plugin-jsx-a11y (max 6.10.2, peers ..^9)
supports ESLint 10, so the honest minimal fix is to pin the linter rather than the
plugins: eslint + @eslint/js ^10 -> ^9.39.5.

  - .npmrc DELETED. The legacy-peer-deps=true workaround added during the chain cut
    existed only because ESLint 10 broke the peer graph. With eslint 9 the graph
    resolves strictly and `npm ci` succeeds with no flags.
  - Caveat: eslint 9.39.5 warns "no longer supported". That is the trade — a
    supported-but-broken linter vs an EOL-but-working one. The real exit is
    upstream; logged in humanpending.md.

With lint actually executing, 23 masked errors surfaced. 14 mechanical ones fixed:
7 no-unnecessary-type-assertion (--fix), 3 prefer-for-of, 2 prefer-optional-chain,
2 require-await, 1 unused eslint-disable (replaced with one that states why the
cast exists). The prefer-optional-chain and prefer-for-of edits land in the
signed-config verifier and in permitDomainKey, so they were applied by hand, not by
a fixer, and proven behaviour-neutral: permit-allowlist + payment-config-verifier +
api-client suites pass 26/26.

The remaining 9 are react-hooks/set-state-in-effect, new in
eslint-plugin-react-hooks v7, flagging a pre-existing pattern in the payment modals
(an effect mirroring a `status` prop onto a local `step` machine). That is a real
refactor of live payment-flow state and does not belong in a CI-repair change, so
the rule is set to `warn` with the rationale inline in eslint.config.js. Lint now
reports 0 errors / 10 warnings and exits 0. No security or correctness rule relaxed.

BUILD — failed in two places, both fixed
1. Tailwind v4 moved its PostCSS plugin out; passing `tailwindcss` directly throws.
   Added @tailwindcss/postcss and repointed postcss.config.js.
2. esbuild was never installed, so Vite died with "Cannot find package 'esbuild'":
   - overrides.esbuild pinned ^0.25.0 while Vite 8.3.1 peers ^0.27.0 || ^0.28.0 —
     an unsatisfiable slot, so npm installed nothing. Raised to ^0.28.1, which also
     clears GHSA-gv7w-rqvm-qjhr (HIGH, esbuild < 0.28.1) and GHSA-g7r4-m6w7-qqqr.
     The old ^0.25.0 pin was itself vulnerable to both.
   - Vite declares esbuild as an OPTIONAL peer, so it is never installed
     transitively. Added as an explicit devDependency.

Verified locally (executed), from a clean node_modules each time
  - npm ci (no flags, no .npmrc): clean.
  - npm run typecheck: PASS.
  - npm run lint: exit 0 — 0 errors, 10 warnings.
  - npx vitest run: 165 passed, 4 failed.
  - npm run build: PASS. dist/ has no solana entry; styles.css emitted.
  - npm audit: 0 vulnerabilities (was 10 high / 16 moderate / 2 low). No --force.
  - Simulated the supply-chain job: `npm ci --ignore-scripts && npm run build`
    succeeds, and `npm pack --dry-run` ships 68 files with no src/ or archive/.

Still failing, deliberately untouched: the 4 signPermit cases in
permit-allowlist.test.ts hit ADR-0004's KNOWN_PERMIT_TOKENS gate with a
non-allow-listed token domain, and the shipped allowlist holds one placeholder
digest. Making them pass means deciding the real allowlist contents. The gate was
NOT weakened and the tests were NOT mocked out.
…approve (HP-5)

The allowlist held one placeholder digest that matched no token, so permit:'auto'
never used a permit and four signPermit tests failed on the ADR-0004 gate.

Now USDC Ethereum, DAI Ethereum and USDC Base (owner decision HP-5), each re-derived
from the chain on 2026-09-30 by scripts/verify-permit-domains.mjs: name()/version()
read from the token, DOMAIN_SEPARATOR() rebuilt from them and matched. ADR-0004 is
not weakened: unknown domains still fall back (auto) or throw (require), and the
set is pinned exactly by a test.

DAI's on-chain PERMIT_TYPEHASH is Permit(holder,spender,nonce,expiry,allowed), not
EIP-2612, so the SDK's typed data and permit(owner,…,v,r,s) call can never be
redeemed by it. detectPermitSupport now reports a token whose PERMIT_TYPEHASH is not
EIP-2612's as unsupported (reason 'non-eip2612-permit'); auto then approves. Tokens
without the view (OpenZeppelin ERC20Permit) are unaffected.

Tests: the four signPermit cases target the allow-listed USDC domain and reach the
guard each is named after (owner mismatch, chain mismatch, malformed signature,
happy path); the unknown-domain refusal has its own test and asserts the wallet is
never asked to sign.
…, HP-16)

The browser could only send plain payInNative/payInToken, so every SDK payment was
detected and settled but attributed to no payment request: no status progression,
no paymentRequestId or metadata on the webhook, no way for the merchant to credit
the right user.

- ABI: payInNativeWithReference(bytes32), payInTokenWithReference(address,uint256,
  bytes32), PaymentReference event. Selectors pinned against the compiled V3.2.3
  artifact (0x817da306 / 0xa7d020be).
- buildPayInCall (pure) + executePayIn*WithReference; the plain executors are
  documented as unattributed.
- resolveAttribution: the one policy every surface applies before the wallet is
  touched. Attributed is the default and refuses without a valid non-zero bytes32
  (MissingPaymentReferenceError); plain payIn* only on mode:'unattributed'.
- usePayment.startPayment(..., { paymentReference, mode }).
- Web3SettleConfig.createPayment(ctx): the modal asks the merchant backend for the
  reference at Pay time with the exact chain/token/amount and userId (previously
  dropped); onSuccess(session).id is the requestId.
- Headless controller start(amount, { paymentReference }) hands it to runPayment;
  <web3settle-pay-button payment-reference> validates and emits it.

BREAKING: removes Web3SettleApiClient.createTopUpSession/getSessionStatus,
CreateSessionResponse(Schema), SESSION_POLL_INTERVAL_MS, MAX_POLL_ATTEMPTS. They
called /api/storefronts/{id}/sessions, which no gateway serves (404), and creating
a payment needs the storefront API key, which must stay server-side.

Tests (199 total, 0 failing): encoding + decoding of both WithReference calls,
malformed/zero references refused, usePayment sends WithReference and never touches
the wallet without a reference, headless runner receives the reference, WC attribute.
…flow

README leads with how a payment reaches an order (server create with metadata,
browser pay with the reference, status, signed v1 webhook crediting
min(amountPaid, amountInvoiced)), drops the session calls, fixes the peer table to
what package.json declares (wagmi ^3.6.16, viem ^2.52.2), and states the known
limitation: the signed payment-config pubkey is still the all-zero placeholder, so
the modal cannot load against a real gateway yet.

Version 0.6.0: the chain cut (HP-3), the removed session API and the new ABI are
breaking on 0.x. Publishes nothing.
@marceloceccon
marceloceccon merged commit 788e9c1 into develop Sep 30, 2026
10 checks passed
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.

1 participant