diff --git a/docs/how-tos/pay-on-behalf/operations.md b/docs/how-tos/pay-on-behalf/operations.md new file mode 100644 index 0000000..280b547 --- /dev/null +++ b/docs/how-tos/pay-on-behalf/operations.md @@ -0,0 +1,277 @@ +--- +id: operations +title: Pay on Behalf Operations +sidebar_label: Operations +--- + +# Pay on Behalf Operations + +All functions on this page accept a `smartAccountClient` built as described in [Setup](./setup). They return a `Promise` containing the **UserOperation hash** (`userOpHash`) — this is not a normal on-chain transaction hash, so it can't be looked up with `publicClient.getTransactionReceipt`. + +:::info Token Registry v5 only +These operations only work against **Token Registry v5 (TR v5)** contracts. +::: + +:::tip Remarks encryption +All `remarks` strings are automatically encrypted with the document ID (`options.id`) before being sent on-chain. Pass the document's `id` in the `TransactionOptions` argument whenever remarks are present. +::: + +## Getting the receipt + +Whenever a section below says "parse it from the receipt," it means the **UserOperation receipt**, fetched from the bundler via `waitForUserOperationReceipt` — not the transaction receipt. The bundler client used to build `smartAccountClient` (see [Setup](./setup)) is decorated with this action: + +```ts +const userOpReceipt = await smartAccountClient.waitForUserOperationReceipt({ + hash: txHash, // the userOpHash returned by any *Gasless function +}); + +// The actual on-chain transaction receipt, containing decodable logs +const { logs } = userOpReceipt.receipt; +``` + +Decode `logs` with the `PlatformPaymaster` ABI to read out emitted events such as `RegistryDeployed` and `TitleEscrowLinked`. + +--- + +## Deploy a Token Registry + +Deploys a new `TradeTrustToken` registry clone through the `PlatformPaymaster`. The caller must have at least 1 deployment credit (`setUserWhitelist`). + +```ts +import { deployTokenRegistryGasless } from '@trustvc/trustvc'; + +const txHash = await deployTokenRegistryGasless( + 'My Shipping Line', // registry name + 'MSL', // registry symbol + smartAccountClient, + { + paymasterAddress: '0xYourPaymaster...', + tokenRegistryImplAddress: '0x64bc665056DC8bE4092e569ED13a7F273Be28cD2', // TDocDeployer on Sepolia + }, +); +``` + +The `RegistryDeployed(user, deployed, creditsLeft)` event is emitted by the paymaster. Parse it from the [UserOperation receipt](#getting-the-receipt) to get the deployed registry address. + +--- + +## Mint a Document + +Mints a new trade document on an authorized registry. Automatically authorizes the beneficiary, holder, and the new `TitleEscrow` on the paymaster. + +```ts +import { mintGasless } from '@trustvc/trustvc'; + +const txHash = await mintGasless( + { + paymasterAddress: '0xYourPaymaster...', + tokenRegistryAddress: '0xYourRegistry...', + }, + smartAccountClient, + { + beneficiaryAddress: '0xBeneficiary...', + holderAddress: '0xHolder...', + tokenId: '0xdeadbeef', // or a BigInt + remarks: 'Initial issuance', // optional + }, + { id: 'document-uuid' }, // used to encrypt remarks +); +``` + +The `TitleEscrowLinked(titleEscrow, registry)` event is emitted. Parse it from the [UserOperation receipt](#getting-the-receipt) to get the deployed `TitleEscrow` address. + +--- + +## Title Escrow Operations + +All operations below target the `TitleEscrow` contract directly. The `smartAccountClient` owner must be the current holder or beneficiary as appropriate. + +### Transfer Holder + +Transfers the **holder** role to a new address. Caller must be the current holder. + +```ts +import { transferHolderGasless } from '@trustvc/trustvc'; + +const txHash = await transferHolderGasless( + { titleEscrowAddress: '0xTitleEscrow...' }, + smartAccountClient, + { + holderAddress: '0xNewHolder...', + remarks: 'Transferring to freight forwarder', // optional + }, + { id: 'document-uuid' }, +); +``` + +### Transfer Beneficiary (Nominate) + +Nominates a new beneficiary. Caller must be the current beneficiary. + +```ts +import { transferBeneficiaryGasless } from '@trustvc/trustvc'; + +const txHash = await transferBeneficiaryGasless( + { titleEscrowAddress: '0xTitleEscrow...' }, + smartAccountClient, + { + newBeneficiaryAddress: '0xNewBeneficiary...', + remarks: 'Endorsing to buyer', // optional + }, + { id: 'document-uuid' }, +); +``` + +### Transfer Both Owners + +Transfers holder and beneficiary in one transaction. Caller must be both holder and beneficiary. + +```ts +import { transferOwnersGasless } from '@trustvc/trustvc'; + +const txHash = await transferOwnersGasless( + { titleEscrowAddress: '0xTitleEscrow...' }, + smartAccountClient, + { + newBeneficiaryAddress: '0xNewBeneficiary...', + newHolderAddress: '0xNewHolder...', + remarks: 'Full transfer', // optional + }, + { id: 'document-uuid' }, +); +``` + +### Nominate a Beneficiary + +Nominates a beneficiary without immediately completing the transfer. The nominated beneficiary must accept separately. + +```ts +import { nominateGasless } from '@trustvc/trustvc'; + +const txHash = await nominateGasless( + { titleEscrowAddress: '0xTitleEscrow...' }, + smartAccountClient, + { + newBeneficiaryAddress: '0xNominatedBeneficiary...', + remarks: 'Nomination for endorsement', // optional + }, + { id: 'document-uuid' }, +); +``` + +### Reject a Pending Transfer — Holder + +Rejects a pending holder transfer. Caller must be the current holder. + +```ts +import { rejectTransferHolderGasless } from '@trustvc/trustvc'; + +const txHash = await rejectTransferHolderGasless( + { titleEscrowAddress: '0xTitleEscrow...' }, + smartAccountClient, + { remarks: 'Rejecting transfer' }, // optional + { id: 'document-uuid' }, +); +``` + +### Reject a Pending Transfer — Beneficiary + +Rejects a pending beneficiary (nomination). Caller must be the current beneficiary. + +```ts +import { rejectTransferBeneficiaryGasless } from '@trustvc/trustvc'; + +const txHash = await rejectTransferBeneficiaryGasless( + { titleEscrowAddress: '0xTitleEscrow...' }, + smartAccountClient, + { remarks: 'Rejecting nomination' }, + { id: 'document-uuid' }, +); +``` + +### Reject a Pending Transfer — Both Owners + +Rejects a pending combined transfer. Caller must be both current holder and beneficiary. + +```ts +import { rejectTransferOwnersGasless } from '@trustvc/trustvc'; + +const txHash = await rejectTransferOwnersGasless( + { titleEscrowAddress: '0xTitleEscrow...' }, + smartAccountClient, + { remarks: 'Rejecting combined transfer' }, + { id: 'document-uuid' }, +); +``` + +--- + +## Return to Issuer + +At the end of a document's lifecycle, the holder (who is also the beneficiary) returns the token to the issuer. + +```ts +import { returnToIssuerGasless } from '@trustvc/trustvc'; + +const txHash = await returnToIssuerGasless( + { titleEscrowAddress: '0xTitleEscrow...' }, + smartAccountClient, + { remarks: 'Surrendering document' }, // optional + { id: 'document-uuid' }, +); +``` + +### Reject a Returned Document + +The platform (registry admin) restores the document back to the title escrow, rejecting the return. + +```ts +import { rejectReturnedGasless } from '@trustvc/trustvc'; + +const txHash = await rejectReturnedGasless( + { tokenRegistryAddress: '0xYourRegistry...' }, + smartAccountClient, + { + tokenId: '0xdeadbeef', + remarks: 'Return rejected', // optional + }, + { id: 'document-uuid' }, +); +``` + +### Accept a Returned Document (Burn) + +The platform accepts the return and burns the document. + +```ts +import { acceptReturnedGasless } from '@trustvc/trustvc'; + +const txHash = await acceptReturnedGasless( + { tokenRegistryAddress: '0xYourRegistry...' }, + smartAccountClient, + { + tokenId: '0xdeadbeef', + remarks: 'Document accepted and destroyed', // optional + }, + { id: 'document-uuid' }, +); +``` + +--- + +## Summary + +| Function | Target | Caller | +|---|---|---| +| `deployTokenRegistryGasless` | PlatformPaymaster | Whitelisted user | +| `mintGasless` | PlatformPaymaster | User with MINTER_ROLE | +| `transferHolderGasless` | TitleEscrow | Current holder | +| `transferBeneficiaryGasless` | TitleEscrow | Current beneficiary | +| `transferOwnersGasless` | TitleEscrow | Holder and beneficiary | +| `nominateGasless` | TitleEscrow | Current beneficiary | +| `rejectTransferHolderGasless` | TitleEscrow | Current holder | +| `rejectTransferBeneficiaryGasless` | TitleEscrow | Current beneficiary | +| `rejectTransferOwnersGasless` | TitleEscrow | Holder and beneficiary | +| `returnToIssuerGasless` | TitleEscrow | Holder = beneficiary | +| `rejectReturnedGasless` | Token Registry | Registry admin | +| `acceptReturnedGasless` | Token Registry | Registry admin | diff --git a/docs/how-tos/pay-on-behalf/overview.md b/docs/how-tos/pay-on-behalf/overview.md new file mode 100644 index 0000000..9b5836b --- /dev/null +++ b/docs/how-tos/pay-on-behalf/overview.md @@ -0,0 +1,142 @@ +--- +id: overview +title: Pay on Behalf (EIP-7702) +sidebar_label: Overview +--- + +# Pay on Behalf with EIP-7702 + +:::caution Beta +Pay on Behalf transactions are currently in **beta**. APIs, contract addresses, and behavior may change before the stable release. Use on testnet only and do not rely on this feature in production. +::: + +:::info Token Registry v5 only +Pay on Behalf is only supported for **Token Registry v5 (TR v5)** contracts deployed via `TDocDeployer`. It is not available for earlier token registry versions. See the [TR v5 migration guide](../../migration-guide/migration-tr-v5) if you are still on an older registry. +::: + +TrustVC supports **Pay on Behalf** — letting a platform (issuer) cover trade document transaction costs for its users. Users can deploy token registries, mint documents, and perform all title escrow operations without holding ETH, while gas is sponsored on their behalf by a **PlatformPaymaster**. + +Under the hood, this is implemented as a **gasless** transaction flow using [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) (smart account delegation) and ERC-4337 account abstraction — the "Pay on Behalf" name describes the outcome for the user (the platform pays), while "gasless" describes the underlying mechanism you'll see referenced in code and function names. + +## Architecture + +The system is built on three smart contracts: + +| Contract | Role | +| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `EIP7702Implementation` | Shared smart account logic. EOAs delegate to this once via a type-4 transaction — their bytecode becomes `0xef0100 \|\| impl_address`, giving them full smart-account capability while keeping the same address and private key. | +| `PlatformPaymaster` | Per-platform ERC-4337 paymaster deployed as a minimal-proxy clone. Sponsors gas for registry deploys, mints, and title escrow operations within configured limits. | +| `PlatformAccountFactory` | Deploys `PlatformPaymaster` clones deterministically via `CREATE2`. Cheap (~55 k gas) and the clone address is predictable before deployment. | + +## How it works + +The flow splits into a one-time **admin setup** (done once by the platform) and the **user experience** (repeated for every action the user takes). The user never sees a gas prompt or needs to hold ETH. + +
+ +Admin setup + +
+ +
+ +![User wallet experience](/docs/payOnBehalf/userWalletExperience.png) + +
+In text form, the same flow: + +``` +[One-time, separate step] EOA delegates to EIP7702Implementation via a type-4 transaction + - User-owned wallet: user signs the authorization off-chain, platform owner submits it + - Platform-managed wallet: platform owner both signs and submits (it holds the key) + +User (EOA, no ETH, already delegated) + │ + │ 1. Submit UserOperation via a bundler + │ + ▼ +Bundler + │ + │ 2. Calls EntryPoint → validates against PlatformPaymaster + │ + ▼ +PlatformPaymaster + │ + ├── Path A — Title escrow / registry calls + │ Checks: caller ∈ authorizedCallers, target ∈ authorizedRegistries or authorizedTitleEscrows + │ Enforces: dailyLimit per user + │ + └── Path B — deployRegistry / mintDocument on the paymaster itself + deployRegistry: userWhitelist[sender] > 0 (platform whitelists users) + mintDocument: caller has MINTER_ROLE on the registry +``` + +## Deployed addresses + +### Sepolia (chainId: 11155111) + +| Contract | Address | +| ---------------------------------- | -------------------------------------------- | +| EIP7702Implementation | `0xa46EC3920Ac5fc54F4bA33185A91ae250aDF59B8` | +| PlatformPaymaster (implementation) | `0xa24695178ea881ab7d4d105106e4906a8da4752b` | +| PlatformAccountFactory | `0x7e9ef6363180baa744eb32ceab367a44f52adc9f` | +| TDocDeployer | `0x64bc665056DC8bE4092e569ED13a7F273Be28cD2` | +| EntryPoint v0.8 | `0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108` | + +### Polygon Amoy (chainId: 80002) + +| Contract | Address | +| ---------------------------------- | -------------------------------------------- | +| EIP7702Implementation | `0x044de1d4515a76ed9e431e8ec89e8d600405fd86` | +| PlatformPaymaster (implementation) | `0x1c4367128933E9a88de26C723F50C288fA0fFea7` | +| PlatformAccountFactory | `0xfbe1d336000d567f98ac5318f7c0144501388409` | +| TDocDeployer | `0xfcafea839e576967b96ad1FBFB52b5CA26cd1D25` | +| EntryPoint v0.8 | `0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108` | + +## SDK package + +The `@trustvc/trustvc` SDK exposes all Pay on Behalf functions under the `eip7702-functions` namespace. Install it once: + +```bash +npm install @trustvc/trustvc@beta +``` + +Import any function directly: + +```ts +import { + deployTokenRegistryGasless, + mintGasless, + transferHolderGasless, + transferBeneficiaryGasless, + transferOwnersGasless, + nominateGasless, + returnToIssuerGasless, + rejectReturnedGasless, + acceptReturnedGasless, + rejectTransferHolderGasless, + rejectTransferBeneficiaryGasless, + rejectTransferOwnersGasless, +} from "@trustvc/trustvc"; +``` + +:::note Naming +These SDK functions keep the `Gasless` suffix (their actual exported names), even though the feature is presented to platforms and end users as **Pay on Behalf**. The suffix refers to the underlying mechanism; the name doesn't change based on which bundler or paymaster infrastructure you use. +::: + +## Prerequisites + +Before calling any Pay on Behalf function you need: + +1. **A PlatformPaymaster deployed for your platform** — see [Setup](./setup). +2. **A bundler/paymaster provider API key** — see [Setup](./setup) for a walkthrough using Pimlico as an example. +3. **The user's EOA delegated** — a separate, one-time type-4 transaction, submitted before the user's first sponsored action (not as part of it). Two ways to get there: + - **User-owned wallet**: the user's wallet signs an EIP-7702 authorization off-chain, and the **platform owner** submits it on-chain (paying its gas) to delegate the EOA to `EIP7702Implementation`. + - **Platform-managed wallet**: the platform owner creates the wallet on the user's behalf and, holding its key, both signs the authorization and submits the delegation itself. +4. **A built `smartAccountClient`** — the permissionless `SmartAccountClient` wrapping the delegated EOA. + +All four are covered in [Setup](./setup). Once set up, jump to [Operations](./operations) for code examples. If you're looking for how this is exposed in the TT-web application's Settings page, see [Pay on Behalf on TT-web](./tt-web-settings). + +## Disclaimer + +Pay on Behalf is one possible way to sponsor a user's transaction costs. This reference implementation uses **EIP-7702 smart account delegation** together with **Pimlico** as an example bundler and paymaster infrastructure provider. Pimlico is referenced here only because it's what this implementation is built and tested against — TrustVC does not require, endorse, or favor Pimlico over any other provider. Any ERC-4337-compatible bundler and paymaster infrastructure that supports EIP-7702 can be substituted. diff --git a/docs/how-tos/pay-on-behalf/setup.md b/docs/how-tos/pay-on-behalf/setup.md new file mode 100644 index 0000000..34bfc1d --- /dev/null +++ b/docs/how-tos/pay-on-behalf/setup.md @@ -0,0 +1,303 @@ +--- +id: setup +title: Setup +sidebar_label: Setup +--- + +# Setup + +This page walks through one-time setup steps: deploying a PlatformPaymaster for your platform, whitelisting users, and building the `smartAccountClient` that all Pay on Behalf SDK functions accept. + +:::info Disclaimer +The steps below use **Pimlico** as the bundler/paymaster infrastructure provider because that's what this reference implementation is built and tested against. This is not an endorsement or requirement — any ERC-4337-compatible bundler that supports EIP-7702 can be substituted, and the `smartAccountClient` construction in step 5 is where you'd swap providers. +::: + +:::caution Beta +Install the beta release of `@trustvc/trustvc` to access the EIP-7702 Pay on Behalf functions: + +```bash +npm install @trustvc/trustvc@beta permissionless viem +``` + +The Pay on Behalf API is not available in the `latest` (`2.x`) release. +::: + +:::info Token Registry v5 only +This setup applies to **Token Registry v5 (TR v5)** deployments only. +::: + +## 0. Get a bundler API key + +A **bundler** submits UserOperations on behalf of users and, together with your `PlatformPaymaster`, is what makes Pay on Behalf work. This guide uses **Pimlico** as a working example — any ERC-4337-compatible bundler that supports EIP-7702 can be used instead. A free account is sufficient for development and testing. + +1. Go to [dashboard.pimlico.io](https://dashboard.pimlico.io) and sign up (GitHub or email). +2. Create a new **API key** from the dashboard. +3. Add it to your `.env`: + +```env +PIMLICO_API_KEY=your_key_here +``` + +The bundler URL is constructed as: + +```text +https://api.pimlico.io/v2/{chainId}/rpc?apikey={PIMLICO_API_KEY} +``` + +| Network | chainId | +| --- | --- | +| Sepolia | `11155111` | +| Polygon Amoy | `80002` | + +Pimlico's free tier has no credit card requirement and is enough to run through this entire guide. + +## 1. Deploy a PlatformPaymaster + +Each platform (issuer) deploys its own `PlatformPaymaster` clone via `PlatformAccountFactory`. The clone is a cheap minimal proxy (~55 k gas) with its own state (owner, daily limit, authorized registries). + +### Using the SDK + +```ts +import { deployPlatformPaymaster } from '@trustvc/trustvc'; +import { createWalletClient, createPublicClient, http } from 'viem'; +import { sepolia } from 'viem/chains'; +import { privateKeyToAccount } from 'viem/accounts'; + +const deployer = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); + +const walletClient = createWalletClient({ + account: deployer, + chain: sepolia, + transport: http(process.env.SEPOLIA_RPC_URL), +}); + +const publicClient = createPublicClient({ + chain: sepolia, + transport: http(process.env.SEPOLIA_RPC_URL), +}); + +const { txHash, paymasterAddress } = await deployPlatformPaymaster( + walletClient, + { + // factoryAddress defaults to the Sepolia PlatformAccountFactory if omitted + platformAddress: deployer.address, // EOA that owns the paymaster + dailyLimit: 0n, // 0 = unlimited; set in wei if you want a cap + salt: `0x${'ab'.repeat(32)}`, // bytes32 CREATE2 salt — must be unique per platform + }, + publicClient, +); + +console.log('Paymaster deployed at:', paymasterAddress); +console.log('Tx:', txHash); +``` + +The function also accepts an ethers v5 or v6 signer in place of the viem `WalletClient`. + +### Options + +| Option | Required | Description | +|---|---|---| +| `salt` | Yes | `bytes32` CREATE2 salt. Use `crypto.randomBytes(32).toString('hex')` for a random one. | +| `platformAddress` | No | Paymaster owner. Defaults to the signer's address. | +| `dailyLimit` | No | Per-user daily gas spend cap in wei. `0n` = unlimited. | +| `factoryAddress` | No | Override factory address. Defaults to Sepolia's deployed factory. | +| `chainId` | No | Used to auto-resolve `factoryAddress`. Defaults to Sepolia. | + +## 2. Stake the paymaster on EntryPoint + +The paymaster must hold a deposit on the EntryPoint to sponsor UserOps. Stake it once after deployment using a funded EOA: + +```ts +import { parseAbi, parseEther, createWalletClient, http } from 'viem'; +import { sepolia } from 'viem/chains'; + +const entryPointAbi = parseAbi([ + 'function depositTo(address account) external payable', + 'function addStake(uint32 unstakeDelaySec) external payable', +]); + +const ENTRY_POINT = '0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108'; + +// Deposit ETH to cover gas sponsorship +await walletClient.writeContract({ + address: ENTRY_POINT, + abi: entryPointAbi, + functionName: 'depositTo', + args: [paymasterAddress], + value: parseEther('0.1'), // adjust as needed +}); + +// Stake for ERC-4337 compliance +await walletClient.writeContract({ + address: ENTRY_POINT, + abi: entryPointAbi, + functionName: 'addStake', + args: [86400], // 1-day unstake delay + value: parseEther('0.001'), + account: deployer, + chain: sepolia, +}); +``` + +## 3. Whitelist users (admin) + +The paymaster owner must whitelist users before they can deploy registries under Pay on Behalf. Credits represent how many registry deployments are allowed per user (max 3). + +```ts +import { setUserWhitelist } from '@trustvc/trustvc'; + +// Whitelist a user with 2 deployment credits +await setUserWhitelist( + ownerWalletClient, // platform owner signer + paymasterAddress, // your PlatformPaymaster + '0xUserAddress...', // user to whitelist + 2n, // credits (0–3) +); +``` + +Other admin functions available from `@trustvc/trustvc`: + +```ts +import { + removeUserFromWhitelist, + addRegistry, + removeRegistry, + addTitleEscrow, + removeTitleEscrow, + addAuthorizedCaller, + removeAuthorizedCaller, + setDailyLimit, +} from '@trustvc/trustvc'; +``` + +All admin functions accept an ethers v5/v6 signer or viem `WalletClient` as the first argument and return `Promise` (tx hash). + +## 4. Delegate the user's EOA (platform owner) + +Delegation is a separate, one-time type-4 transaction — it is **not** bundled into the user's first UserOperation. The EOA must already be delegated to `EIP7702Implementation` before any `*Gasless` function is called for it. There are two ways to get there, depending on who holds the EOA's private key: + +**User-owned wallet** — the user's wallet signs the authorization off-chain (no gas), and the platform owner submits it on-chain, paying the gas: + +```ts +// 1. User's wallet signs the authorization (off-chain, no gas) +const authorization = await userWalletClient.signAuthorization({ + contractAddress: EIP7702_IMPLEMENTATION_ADDRESS, // see Overview > Deployed addresses +}); + +// 2. Platform owner submits it as a type-4 transaction, paying the gas +const delegationTxHash = await platformOwnerWalletClient.sendTransaction({ + authorizationList: [authorization], + to: userEoaAddress, + value: 0n, +}); +``` + +**Platform-managed wallet** — if the platform created and holds the key for the user's wallet, the platform owner both signs and submits, since it controls the EOA directly: + +```ts +const authorization = await platformManagedUserWalletClient.signAuthorization({ + contractAddress: EIP7702_IMPLEMENTATION_ADDRESS, +}); + +const delegationTxHash = await platformManagedUserWalletClient.sendTransaction({ + authorizationList: [authorization], + to: platformManagedUserWalletClient.account.address, + value: 0n, +}); +``` + +## 5. Build a smart account client + +All Pay on Behalf SDK functions take a `smartAccountClient` as their second argument. Build one from the user's delegated EOA using **permissionless** + **Pimlico** (or your chosen bundler): + +```ts +import { + createPublicClient, + createWalletClient, + custom, + http, +} from 'viem'; +import { sepolia } from 'viem/chains'; +import { entryPoint08Address } from 'viem/account-abstraction'; +import { createPimlicoClient } from 'permissionless/clients/pimlico'; +import { createSmartAccountClient } from 'permissionless'; +import { to7702SimpleSmartAccount } from 'permissionless/accounts'; + +const PIMLICO_URL = `https://api.pimlico.io/v2/${sepolia.id}/rpc?apikey=${process.env.PIMLICO_API_KEY}`; +// TrustVC's EIP7702Implementation on Sepolia — see Overview > Deployed addresses. +// Must match the contract the EOA was delegated to in step 4, not permissionless's default. +const EIP7702_IMPLEMENTATION_ADDRESS = '0xa46EC3920Ac5fc54F4bA33185A91ae250aDF59B8'; + +async function buildSmartAccountClient(ownerAddress: `0x${string}`, paymasterAddress: `0x${string}`) { + const publicClient = createPublicClient({ chain: sepolia, transport: http(process.env.SEPOLIA_RPC_URL) }); + + // walletClient wraps the user's signer (MetaMask, hardware wallet, etc.) + const walletClient = createWalletClient({ + account: ownerAddress, + chain: sepolia, + transport: custom(window.ethereum), + }); + + const pimlicoClient = createPimlicoClient({ + transport: http(PIMLICO_URL), + entryPoint: { address: entryPoint08Address, version: '0.8' }, + }); + + // Wraps the delegated EOA as an EIP-7702 smart account. + // accountLogicAddress must match the implementation the EOA was delegated to (step 4) — + // without it, this defaults to permissionless's own implementation, not TrustVC's. + const account = await to7702SimpleSmartAccount({ + client: publicClient, + owner: walletClient, + accountLogicAddress: EIP7702_IMPLEMENTATION_ADDRESS, + }); + + const smartAccountClient = createSmartAccountClient({ + account, + chain: sepolia, + bundlerTransport: http(PIMLICO_URL), + client: publicClient, + // PlatformPaymaster — validates on-chain, no off-chain signature needed + paymaster: { + async getPaymasterStubData() { + return { + paymaster: paymasterAddress, + paymasterData: '0x' as `0x${string}`, + paymasterVerificationGasLimit: 300_000n, + paymasterPostOpGasLimit: 150_000n, + isFinal: false, + }; + }, + async getPaymasterData() { + return { + paymaster: paymasterAddress, + paymasterData: '0x' as `0x${string}`, + paymasterVerificationGasLimit: 300_000n, + paymasterPostOpGasLimit: 150_000n, + }; + }, + }, + userOperation: { + estimateFeesPerGas: async () => { + const { fast } = await pimlicoClient.getUserOperationGasPrice(); + return { maxFeePerGas: fast.maxFeePerGas, maxPriorityFeePerGas: fast.maxPriorityFeePerGas }; + }, + }, + }); + + return { smartAccountClient, publicClient }; +} +``` + +:::note +`to7702SimpleSmartAccount` wraps an EOA as a smart account for building and signing UserOperations — it does not perform delegation. The EOA must already be delegated to `EIP7702Implementation` beforehand, via the separate type-4 transaction described in [step 4](#4-delegate-the-users-eoa-platform-owner). +::: + +## Required environment variables + +| Variable | Description | +|---|---| +| `PRIVATE_KEY` | Platform owner private key (pays deployment gas) | +| `SEPOLIA_RPC_URL` | Sepolia RPC endpoint | +| `PIMLICO_API_KEY` | Pimlico bundler API key — free tier at [dashboard.pimlico.io](https://dashboard.pimlico.io) | +| `PAYMASTER_ADDRESS` | Deployed `PlatformPaymaster` clone address | diff --git a/docs/how-tos/pay-on-behalf/tt-web-settings.md b/docs/how-tos/pay-on-behalf/tt-web-settings.md new file mode 100644 index 0000000..71da21c --- /dev/null +++ b/docs/how-tos/pay-on-behalf/tt-web-settings.md @@ -0,0 +1,56 @@ +--- +id: tt-web-settings +title: Pay on Behalf on TT-web +sidebar_label: TT-web Settings +--- + +# Pay on Behalf on TT-web + +TT-web (the TradeTrust web application) exposes Pay on Behalf configuration through its **Settings** panel, accessible from the gear icon on the top navigation bar. Settings is organized into tabs; **Pay on Behalf** is the fourth tab, alongside: + +| Tab | Purpose | +| ----------------- | --------------------------------------------------------------------------------------------------------------------- | +| General | Application-wide preferences. | +| Network | Blockchain network and RPC configuration. | +| Address Resolver | Configure address resolution for identifying wallet addresses — see [Address Resolver](../advanced/address-resolver). | +| **Pay on Behalf** | Configure and enable sponsored (gas-free) transactions for your users. | + +:::info Token Registry v5 only +The Pay on Behalf tab only applies to documents issued from **Token Registry v5 (TR v5)** registries. It has no effect on earlier registry versions. +::: + +## What the Pay on Behalf tab does + +This tab lets a platform admin turn on Pay on Behalf for their TT-web deployment without writing any code. It's a UI wrapper around the same underlying setup described in [Setup](./setup): + +- Point TT-web at your deployed `PlatformPaymaster` address. +- Provide the bundler/paymaster infrastructure credentials (for example, a Pimlico API key) used to submit sponsored transactions. +- Enable or disable Pay on Behalf for the connected registry. + +Values entered here should match what you configured on-chain in [Setup](./setup) — the tab does not deploy a `PlatformPaymaster` or whitelist users for you; those steps still happen via the SDK/admin functions described there. + +:::info Disclaimer +Pay on Behalf is one possible way to sponsor a user's transaction costs. The TT-web implementation of this tab is built and tested against **Pimlico** as an example bundler/paymaster provider. This is not an endorsement of, or dependency on, Pimlico specifically — any ERC-4337-compatible provider that supports EIP-7702 can be used, as long as it's wired up through the same [Setup](./setup) steps. +::: + +## User experience + +Once an admin has enabled Pay on Behalf, users of that platform's TT-web instance can check their own eligibility from the same tab: + +1. Once the user drops the document to act on, TT-web checks whether the connected wallet address already has EIP-7702 delegation enabled. If it does, a message appears prompting the user to paste the platform's `PlatformPaymaster` address. + + ![Delegation enabled detection](/docs/payOnBehalf/delegationEnabledDetection.png) + +2. The user enters the platform's `PlatformPaymaster` address (provided by the issuer) into the input field on the Pay on Behalf tab. +3. TT-web checks the connected wallet against that paymaster's whitelist and reports back whether the wallet is eligible (whitelisted) for sponsored transactions. + + ![Paymaster enabled](/docs/payOnBehalf/paymasterEnabled.png) + +4. If eligible, supported actions (minting, transferring, etc.) prompt the user's wallet for a **signature** (of the UserOperation) instead of the usual gas-payment transaction confirmation — no ETH is required from the user. + + Metamask signature request + +5. If not eligible, TT-web surfaces this so the user knows to request access from their platform admin — admins grant eligibility by whitelisting the address via `setUserWhitelist` (see [Setup](./setup)). +6. If delegation was not already enabled in step 1, the one-time EIP-7702 delegation happens transparently as part of the user's first sponsored action. + +See [Overview](./overview) for a diagram of the full admin-setup vs. user-experience flow, and [Operations](./operations) for the underlying SDK calls TT-web makes on the user's behalf. diff --git a/sidebars.json b/sidebars.json index e932bde..524b297 100755 --- a/sidebars.json +++ b/sidebars.json @@ -80,6 +80,16 @@ ], "How Tos": [ "how-tos/deployment", + { + "label": "Pay on Behalf (EIP-7702)", + "type": "category", + "items": [ + "how-tos/pay-on-behalf/overview", + "how-tos/pay-on-behalf/setup", + "how-tos/pay-on-behalf/operations", + "how-tos/pay-on-behalf/tt-web-settings" + ] + }, { "label": "Decentralized Renderer", "type": "category", diff --git a/static/docs/payOnBehalf/AdminSetup.png b/static/docs/payOnBehalf/AdminSetup.png new file mode 100644 index 0000000..56ef8fc Binary files /dev/null and b/static/docs/payOnBehalf/AdminSetup.png differ diff --git a/static/docs/payOnBehalf/delegationEnabledDetection.png b/static/docs/payOnBehalf/delegationEnabledDetection.png new file mode 100644 index 0000000..a5e9749 Binary files /dev/null and b/static/docs/payOnBehalf/delegationEnabledDetection.png differ diff --git a/static/docs/payOnBehalf/metamaskSignatureRequest.png b/static/docs/payOnBehalf/metamaskSignatureRequest.png new file mode 100644 index 0000000..bd28b38 Binary files /dev/null and b/static/docs/payOnBehalf/metamaskSignatureRequest.png differ diff --git a/static/docs/payOnBehalf/paymasterEnabled.png b/static/docs/payOnBehalf/paymasterEnabled.png new file mode 100644 index 0000000..c30e66f Binary files /dev/null and b/static/docs/payOnBehalf/paymasterEnabled.png differ diff --git a/static/docs/payOnBehalf/userWalletExperience.png b/static/docs/payOnBehalf/userWalletExperience.png new file mode 100644 index 0000000..5292990 Binary files /dev/null and b/static/docs/payOnBehalf/userWalletExperience.png differ