diff --git a/README.md b/README.md index 818ca85..78c3c83 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ The current source includes: - [`docs/release-evidence-index.md`](docs/release-evidence-index.md) — claim-to-evidence map for reviewers, contributors, auditors, partners, and funders - [`docs/grant-reviewer-guide.md`](docs/grant-reviewer-guide.md) — concise reviewer routes, safety boundaries, and known limitations - [`docs/reviewer-publication-runbook.md`](docs/reviewer-publication-runbook.md) — exact-source manifest and owner-gated publication procedure +- [`docs/base-builder-attribution.md`](docs/base-builder-attribution.md) — owner-gated Base.dev registration, ERC-8021 configuration and verification - [`GOVERNANCE.md`](GOVERNANCE.md) — current decision process, roles, conflicts, funding independence, and succession direction - [`MAINTAINERS.md`](MAINTAINERS.md) — current maintainers and the path to shared stewardship - [`SECURITY.md`](SECURITY.md) — private vulnerability reporting and safe research boundaries diff --git a/docs/base-builder-attribution.md b/docs/base-builder-attribution.md new file mode 100644 index 0000000..de4188a --- /dev/null +++ b/docs/base-builder-attribution.md @@ -0,0 +1,55 @@ +# Base Builder Code attribution + +OpenEscrow can append an ERC-8021 attribution suffix to user-initiated Base transactions after the +owner registers and verifies the app on Base.dev. Attribution is disabled by default and no Builder +Code is hardcoded in source. + +## Configuration + +1. Register and verify `https://openescrow.io` on . +2. Review Base's current terms, select the real payout address and copy the Builder Code from + **Settings → Builder Code**. +3. Set the public build-time environment variable: + + ```dotenv + VITE_BASE_BUILDER_CODE=bc_owner_issued_value + ``` + +4. Build and deploy through the normal reviewed release process. + +The value is a public attribution identifier, not a private key or secret. It must still come from +the owner-controlled Base.dev project and must not be guessed or fabricated. + +## Transaction coverage + +- Direct Privy transactions receive the suffix through Privy's `dataSuffix` plugin. +- Wagmi contract writes receive the same suffix through viem's per-transaction `dataSuffix` + parameter. This explicit path is required because Privy's plugin is not currently applied to + transactions sent through the `@privy-io/wagmi` adapter. +- When the environment variable is blank, both paths behave exactly as before and append nothing. + +No contract upgrade or redeployment is required. ERC-8021 data is appended to calldata and ignored +by the called contract while Base's offchain indexer records the app attribution. + +## Verification + +After deployment, submit one authentic Base Sepolia lifecycle transaction—never synthetic volume +for rewards—and preserve its transaction hash. Verify it in both places: + +1. Base's Builder Code validation tool confirms the expected code and ERC-8021 marker. +2. Base.dev shows the attributed transaction in OpenEscrow's onchain analytics after indexing. + +The testnet/mainnet distinction remains explicit. Attribution evidence is not proof of production +adoption, a grant, or a reward, and no reward should be recorded until an attributable payment is +received. + +## Local checks + +```powershell +cd frontend +npm run test:client-logic +npm run build +``` + +The client-logic suite verifies that an unset or whitespace-only value disables attribution and +that an owner-supplied code is trimmed and encoded with the official `ox/erc8021` utility. diff --git a/frontend/.env.example b/frontend/.env.example index b2ef68c..283b984 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -2,6 +2,11 @@ # Leave unset to retain OpenEscrow's direct injected-wallet fallback. VITE_PRIVY_APP_ID= +# Optional Base.dev Builder Code (for example, bc_...). When set, OpenEscrow +# appends an ERC-8021 attribution suffix to user-initiated Base transactions. +# Obtain the real code by verifying the app on base.dev; never invent one. +VITE_BASE_BUILDER_CODE= + # Real card/bank funding must target a supported mainnet stablecoin. Keep this # disabled for Base Sepolia, where the free faucet remains the funding method. VITE_FIAT_ONRAMP_ENABLED=false diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d2197b1..e256278 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "@privy-io/react-auth": "^3.35.2", "@privy-io/wagmi": "^4.0.15", "@tanstack/react-query": "^5.101.4", + "ox": "1.6.2", "react": "^19.2.7", "react-dom": "^19.2.7", "viem": "2.55.5", @@ -61,6 +62,47 @@ "zustand": "5.0.3" } }, + "node_modules/@base-org/account/node_modules/ox": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.9.tgz", + "integrity": "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.10.1", + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", + "abitype": "^1.0.6", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@base-org/account/node_modules/ox/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@base-org/account/node_modules/zustand": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", @@ -2183,6 +2225,62 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/post-quantum": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.6.1.tgz", + "integrity": "sha512-+pormrDZwjRw05U8ADK4JpHejo87+gBd+muRBB/ozztH5yhDLMDF4jHQWN3NQQAsu1zBNPWTG0ZwVI0CR29H0A==", + "license": "MIT", + "dependencies": { + "@noble/ciphers": "~2.2.0", + "@noble/curves": "~2.2.0", + "@noble/hashes": "~2.2.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/post-quantum/node_modules/@noble/ciphers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.2.0.tgz", + "integrity": "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/post-quantum/node_modules/@noble/curves": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", + "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.2.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/post-quantum/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@oxc-project/types": { "version": "0.139.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", @@ -6598,6 +6696,47 @@ "integrity": "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==", "license": "MIT" }, + "node_modules/@wagmi/connectors/node_modules/ox": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.9.tgz", + "integrity": "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.10.1", + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", + "abitype": "^1.0.6", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@wagmi/connectors/node_modules/ox/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@wagmi/connectors/node_modules/pino": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/pino/-/pino-7.11.0.tgz", @@ -7558,17 +7697,6 @@ "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", "license": "MIT" }, - "node_modules/brotli-wasm": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/brotli-wasm/-/brotli-wasm-3.0.1.tgz", - "integrity": "sha512-U3K72/JAi3jITpdhZBqzSUq+DUY697tLxOuFXB+FpAE/Ug+5C3VZrv4uA674EUZHxNAuQ9wETXNqQkxZD6oL4A==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "engines": { - "node": ">=v18.0.0" - } - }, "node_modules/bs58": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", @@ -9663,9 +9791,9 @@ "license": "MIT" }, "node_modules/ox": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.9.tgz", - "integrity": "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/ox/-/ox-1.6.2.tgz", + "integrity": "sha512-Jcll+tKlUV0sUPJ6YlmcpKRcefipswF5DI9lz+kn8Qc6fY5UrMEoeL+E9Y+9h6apKnvQ4N4anggJWNqFC7Vxiw==", "funding": [ { "type": "github", @@ -9674,13 +9802,16 @@ ], "license": "MIT", "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^2.2.0", + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", + "@noble/post-quantum": "^0.6.1", + "@scure/bip32": "^2.0.0", + "@scure/bip39": "^2.0.0", + "abitype": "^1.3.0", + "eventemitter3": "5.0.1", + "zod": "^4.4.3" }, "peerDependencies": { "typescript": ">=5.4.0" @@ -9691,18 +9822,89 @@ } } }, + "node_modules/ox/node_modules/@noble/ciphers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.3.0.tgz", + "integrity": "sha512-Clu/xdfgVTf9o7ngLOURaxePwR0j8sjclKEtVij10/jGulwFsPWCvvRgG/XjUVf8Nei+jLG6uwyXzUTGY1DQrw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/@noble/curves": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.3.0.tgz", + "integrity": "sha512-v7cY+4oWYPQszRj6ZFGzTVL7uP2TaLo1xMhWHzYC5wj0ZhOXQ5x+sBre8rF3hi8cAoi0bh1qXoovoOkdFtvqEg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.3.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/ox/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", + "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", "license": "MIT", "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, + "node_modules/ox/node_modules/@scure/base": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.3.0.tgz", + "integrity": "sha512-NsG6Y03tY6R5BUis4FdVtHVkur0U6FOzskgs9ZXNl78CUc9fkZ78HmENUle1nSOkCasDmbubmWD9qwB7mm4PZA==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/@scure/bip32": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.3.0.tgz", + "integrity": "sha512-mMPjNcXxJsvNveIgRIXrnwd4omu0wdXo4JowAUZO7/82+/bpAwVltclx6MG6nv2M3dA6IbfVv4xwWxaREm3OcA==", + "license": "MIT", + "dependencies": { + "@noble/curves": "2.3.0", + "@noble/hashes": "2.3.0", + "@scure/base": "2.3.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/@scure/bip39": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.3.0.tgz", + "integrity": "sha512-qdyWuxoYwi3+YmqIsfkpz1I029m980WkVPilj+kG7VxSm+gKQ2BmQru3nv3LbMtpSUXuyZwdFT65JkpKu5OHOQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.3.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/oxlint": { "version": "1.75.0", "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.75.0.tgz", @@ -11453,74 +11655,6 @@ } } }, - "node_modules/wagmi/node_modules/@base-org/account": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/@base-org/account/-/account-2.5.9.tgz", - "integrity": "sha512-05ew8uOx85/wF6BCTzMA/OFW/aYSqJ+38fNC5P5/Q2YVxY8DEBC4hybqo+IMqJIcVy8Eilk2GXCGTkxWzAzCgw==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "@coinbase/cdp-sdk": "^1.48.3", - "brotli-wasm": "^3.0.0", - "clsx": "1.2.1", - "eventemitter3": "5.0.1", - "idb-keyval": "6.2.1", - "ox": "0.6.9", - "preact": "10.24.2", - "viem": "^2.31.7", - "zustand": "5.0.3" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/wagmi/node_modules/@base-org/account/node_modules/zustand": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", - "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } - }, - "node_modules/wagmi/node_modules/@coinbase/wallet-sdk": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/@coinbase/wallet-sdk/-/wallet-sdk-4.3.7.tgz", - "integrity": "sha512-z6e5XDw6EF06RqkeyEa+qD0dZ2ZbLci99vx3zwDY//XO8X7166tqKJrR2XlQnzVmtcUuJtCd5fCvr9Cu6zzX7w==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "@noble/hashes": "^1.4.0", - "clsx": "^1.2.1", - "eventemitter3": "^5.0.1", - "preact": "^10.24.2", - "viem": "^2.27.2" - } - }, "node_modules/wagmi/node_modules/@wagmi/connectors": { "version": "8.0.24", "resolved": "https://registry.npmjs.org/@wagmi/connectors/-/connectors-8.0.24.tgz", diff --git a/frontend/package.json b/frontend/package.json index 372a075..4251337 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -77,6 +77,7 @@ "@privy-io/react-auth": "^3.35.2", "@privy-io/wagmi": "^4.0.15", "@tanstack/react-query": "^5.101.4", + "ox": "1.6.2", "react": "^19.2.7", "react-dom": "^19.2.7", "viem": "2.55.5", diff --git a/frontend/src/AppProviders.tsx b/frontend/src/AppProviders.tsx index 0c587c3..21e05a1 100644 --- a/frontend/src/AppProviders.tsx +++ b/frontend/src/AppProviders.tsx @@ -1,7 +1,8 @@ import type { ReactNode } from "react"; -import { PrivyProvider } from "@privy-io/react-auth"; +import { PrivyProvider, dataSuffix } from "@privy-io/react-auth"; import { baseSepolia } from "wagmi/chains"; import { PRIVY_APP_ID } from "./lib/accountConfig"; +import { BASE_BUILDER_DATA_SUFFIX } from "./lib/baseBuilderConfig"; export function AppProviders({ children }: { children: ReactNode }) { return ( @@ -30,6 +31,9 @@ export function AppProviders({ children }: { children: ReactNode }) { }, defaultChain: baseSepolia, supportedChains: [baseSepolia], + plugins: BASE_BUILDER_DATA_SUFFIX + ? [dataSuffix(BASE_BUILDER_DATA_SUFFIX)] + : [], }} > {children} diff --git a/frontend/src/components/CreateAgreementForm.tsx b/frontend/src/components/CreateAgreementForm.tsx index a0c67a5..14e967d 100644 --- a/frontend/src/components/CreateAgreementForm.tsx +++ b/frontend/src/components/CreateAgreementForm.tsx @@ -25,6 +25,7 @@ import { YIELD_USDC_ADDRESS, chain, } from "../contracts/config"; +import { BASE_BUILDER_DATA_SUFFIX } from "../lib/baseBuilderConfig"; import { formatUSDC, parseUSDC } from "../lib/format"; import { CALIFORNIA_POLICY, @@ -1842,6 +1843,7 @@ function AgreementForm({ functionName: "createMultiTenantAgreementWithToken", account: address, chain, + dataSuffix: BASE_BUILDER_DATA_SUFFIX, args: [ tenantWallets, draft.tenants.map((tenant) => tenant.depositShareBps), diff --git a/frontend/src/components/TxButton.tsx b/frontend/src/components/TxButton.tsx index 64608ba..591e1f9 100644 --- a/frontend/src/components/TxButton.tsx +++ b/frontend/src/components/TxButton.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useAccount, useWaitForTransactionReceipt, useWriteContract } from "wagmi"; import type { Abi } from "viem"; import { chain } from "../contracts/config"; +import { BASE_BUILDER_DATA_SUFFIX } from "../lib/baseBuilderConfig"; import { createSubmittedCallbackSlot } from "../lib/submittedCallback"; import { transactionTerminalState } from "../lib/transactionTerminalState"; @@ -91,7 +92,15 @@ export function TxButton({ onSubmit?.(); setSubmitted(true); reset(); - writeContract({ address, abi, functionName, args, account, chain }); + writeContract({ + address, + abi, + functionName, + args, + account, + chain, + dataSuffix: BASE_BUILDER_DATA_SUFFIX, + }); } catch (cause) { submittedSuccessCallback.clear(); setSubmitted(false); diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts index 40a3a90..b8234b3 100644 --- a/frontend/src/env.d.ts +++ b/frontend/src/env.d.ts @@ -1,6 +1,7 @@ /// interface ImportMetaEnv { + readonly VITE_BASE_BUILDER_CODE?: string; readonly VITE_PRIVY_APP_ID?: string; readonly VITE_FIAT_ONRAMP_ENABLED?: string; readonly VITE_FIAT_ONRAMP_ENVIRONMENT?: "sandbox" | "production"; diff --git a/frontend/src/lib/baseBuilderAttribution.test.ts b/frontend/src/lib/baseBuilderAttribution.test.ts new file mode 100644 index 0000000..51079a0 --- /dev/null +++ b/frontend/src/lib/baseBuilderAttribution.test.ts @@ -0,0 +1,21 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { Attribution } from "ox/erc8021"; +import { createBaseBuilderDataSuffix } from "./baseBuilderAttribution.ts"; + +test("builder attribution stays disabled without an owner-issued code", () => { + assert.equal(createBaseBuilderDataSuffix(), undefined); + assert.equal(createBaseBuilderDataSuffix(" "), undefined); +}); + +test("builder attribution trims and encodes the supplied Base.dev code", () => { + const expected = Attribution.toDataSuffix({ codes: ["bc_openescrow"] }); + assert.equal(createBaseBuilderDataSuffix(" bc_openescrow "), expected); +}); + +test("builder attribution fails closed when the code cannot fit the schema", () => { + assert.throws( + () => createBaseBuilderDataSuffix("x".repeat(256)), + /could not be encoded as an ERC-8021 Builder Code/, + ); +}); diff --git a/frontend/src/lib/baseBuilderAttribution.ts b/frontend/src/lib/baseBuilderAttribution.ts new file mode 100644 index 0000000..2786e67 --- /dev/null +++ b/frontend/src/lib/baseBuilderAttribution.ts @@ -0,0 +1,19 @@ +import { Attribution } from "ox/erc8021"; +import type { Hex } from "viem"; + +/** Build the ERC-8021 suffix for one owner-issued Base.dev Builder Code. */ +export function createBaseBuilderDataSuffix( + rawBuilderCode?: string, +): Hex | undefined { + const builderCode = rawBuilderCode?.trim(); + if (!builderCode) return undefined; + + try { + return Attribution.toDataSuffix({ codes: [builderCode] }); + } catch (cause) { + throw new Error( + "VITE_BASE_BUILDER_CODE could not be encoded as an ERC-8021 Builder Code.", + { cause }, + ); + } +} diff --git a/frontend/src/lib/baseBuilderConfig.ts b/frontend/src/lib/baseBuilderConfig.ts new file mode 100644 index 0000000..28b8601 --- /dev/null +++ b/frontend/src/lib/baseBuilderConfig.ts @@ -0,0 +1,8 @@ +import { createBaseBuilderDataSuffix } from "./baseBuilderAttribution"; + +export const BASE_BUILDER_CODE = + import.meta.env.VITE_BASE_BUILDER_CODE?.trim() ?? ""; + +export const BASE_BUILDER_DATA_SUFFIX = createBaseBuilderDataSuffix( + BASE_BUILDER_CODE, +); diff --git a/frontend/src/testing/privyReactAuthMock.tsx b/frontend/src/testing/privyReactAuthMock.tsx index 9abc2e5..e0a42a0 100644 --- a/frontend/src/testing/privyReactAuthMock.tsx +++ b/frontend/src/testing/privyReactAuthMock.tsx @@ -242,3 +242,7 @@ export function useCreateWallet() { const { createWallet } = useMockPrivyContext(); return { createWallet }; } + +export function dataSuffix(value: `0x${string}`) { + return { name: "dataSuffix", value }; +}