-
Notifications
You must be signed in to change notification settings - Fork 6
YNU-864: Compat layer correctness + cookbooks #148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ihsraham
wants to merge
9
commits into
docs/reorg-for-v1
Choose a base branch
from
feat/ynu-864-compat-correctness-cookbooks
base: docs/reorg-for-v1
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
0bc1fe1
YNU-864: Compat layer correctness + cookbooks
ihsraham bc8e061
YNU-864: Fix compat codemod migration path
ihsraham a46b9d0
YNU-864: Add MCP-assisted migration preview
ihsraham 123188b
YNU-864: Expand MCP client setup docs
ihsraham 0100e85
YNU-864: Add Build examples page
ihsraham 371d2c3
YNU-864: Polish Build examples cards
ihsraham 038d93f
YNU-864: Center Build example screenshots
ihsraham d6ddb4c
YNU-864: Address compat docs review
ihsraham d51fe50
docs: address compat review comments
ihsraham File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| --- | ||
| title: Examples | ||
| description: Deployed Nitrolite example applications, their source repositories, stacks, and SDK usage. | ||
| sidebar_position: 4 | ||
| --- | ||
|
|
||
| # Examples | ||
|
|
||
| These deployed examples show different ways to build on Nitrolite. Use the live apps to understand the product flow, then inspect the source repositories for integration details. | ||
|
|
||
| :::note | ||
| Stack and SDK details reflect the linked repositories when this page was added. The repositories are the source of truth if an example changes after deployment. | ||
| ::: | ||
|
|
||
| <div className="nitrolite-example-list"> | ||
| <div className="card nitrolite-example-card"> | ||
| <div className="nitrolite-example-card__layout"> | ||
| <a className="nitrolite-example-card__media" href="https://cosign-demo-two.vercel.app/" target="_blank" rel="noopener noreferrer"> | ||
| <img className="nitrolite-example-card__image" src="/img/nitrolite/examples/cosign-demo.png" alt="Co-Sign Checkout front page" /> | ||
| </a> | ||
| <div className="nitrolite-example-card__content"> | ||
| <div className="card__header"> | ||
| <h2>Co-Sign Checkout</h2> | ||
| </div> | ||
| <div className="card__body"> | ||
| <p className="nitrolite-example-card__summary"> | ||
| A shared checkout demo where two participants create a cart, co-sign checkout actions, | ||
| move funds into an app-session cart, propose purchases, close the cart, and withdraw | ||
| remaining shared-wallet funds. | ||
| </p> | ||
| <div className="nitrolite-example-card__details"> | ||
| <div> | ||
| <h3>Functionality</h3> | ||
| <ul> | ||
| <li>Two-party shared cart and approval flow.</li> | ||
| <li>Shared Wallet plus Checkout Cart app session.</li> | ||
| <li>Purchase proposals, cart close, and wallet withdrawal.</li> | ||
| <li>YUSD and YELLOW on Ethereum Sepolia.</li> | ||
| </ul> | ||
| </div> | ||
| <div> | ||
| <h3>Stack</h3> | ||
| <ul> | ||
| <li>Next.js, React, TypeScript, Tailwind CSS.</li> | ||
| <li>Supabase rooms, proposals, and events.</li> | ||
| <li>Vercel deployment with scheduled proposal expiration.</li> | ||
| <li>MetaMask SDK and viem.</li> | ||
| </ul> | ||
| </div> | ||
| <div> | ||
| <h3>SDKs</h3> | ||
| <ul> | ||
| <li><code>@yellow-org/sdk</code></li> | ||
| <li><code>@yellow-org/sdk-compat</code></li> | ||
| </ul> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div className="card__footer"> | ||
| <a className="button button--primary button--sm margin-right--sm" href="https://cosign-demo-two.vercel.app/" target="_blank" rel="noopener noreferrer">Live demo</a> | ||
| <a className="button button--secondary button--sm" href="https://github.com/layer-3/cosign-demo" target="_blank" rel="noopener noreferrer">Source</a> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="card nitrolite-example-card"> | ||
| <div className="nitrolite-example-card__layout"> | ||
| <a className="nitrolite-example-card__media" href="https://nitrolite-go-example-production.up.railway.app/" target="_blank" rel="noopener noreferrer"> | ||
| <img className="nitrolite-example-card__image" src="/img/nitrolite/examples/nitrolite-store.png" alt="Nitrolite Store front page" /> | ||
| </a> | ||
| <div className="nitrolite-example-card__content"> | ||
| <div className="card__header"> | ||
| <h2>Nitrolite Store</h2> | ||
| </div> | ||
| <div className="card__body"> | ||
| <p className="nitrolite-example-card__summary"> | ||
| A content-store reference app with a Go backend and browser frontend. The shopper connects | ||
| MetaMask, opens a store app session, adds funds, buys catalog content, reads purchased | ||
| content, and withdraws the remaining balance. | ||
| </p> | ||
| <div className="nitrolite-example-card__details"> | ||
| <div> | ||
| <h3>Functionality</h3> | ||
| <ul> | ||
| <li>MetaMask shopper identity with backend app signing.</li> | ||
| <li>Frontend-constructed app-session updates.</li> | ||
| <li>Catalog purchase and content gating.</li> | ||
| <li>YUSD and YELLOW pricing on Ethereum Sepolia.</li> | ||
| </ul> | ||
| </div> | ||
| <div> | ||
| <h3>Stack</h3> | ||
| <ul> | ||
| <li>Go service serving API and built web UI.</li> | ||
| <li>React, Vite, TypeScript frontend.</li> | ||
| <li>SQLite persistence, Docker, Railway.</li> | ||
| <li>MetaMask and viem in the browser.</li> | ||
| </ul> | ||
| </div> | ||
| <div> | ||
| <h3>SDKs</h3> | ||
| <ul> | ||
| <li><code>github.com/layer-3/nitrolite/sdk/go</code></li> | ||
| <li><code>@yellow-org/sdk</code></li> | ||
| </ul> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div className="card__footer"> | ||
| <a className="button button--primary button--sm margin-right--sm" href="https://nitrolite-go-example-production.up.railway.app/" target="_blank" rel="noopener noreferrer">Live demo</a> | ||
| <a className="button button--secondary button--sm" href="https://github.com/layer-3/nitrolite-store-example" target="_blank" rel="noopener noreferrer">Source</a> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="card nitrolite-example-card"> | ||
| <div className="nitrolite-example-card__layout"> | ||
| <a className="nitrolite-example-card__media" href="https://nexus-testnet-sepolia-ten.vercel.app/" target="_blank" rel="noopener noreferrer"> | ||
| <img className="nitrolite-example-card__image" src="/img/nitrolite/examples/nexus-p2p-transfer.png" alt="Nexus P2P Transfer front page" /> | ||
| </a> | ||
| <div className="nitrolite-example-card__content"> | ||
| <div className="card__header"> | ||
| <h2>Nexus P2P Transfer</h2> | ||
| </div> | ||
| <div className="card__body"> | ||
| <p className="nitrolite-example-card__summary"> | ||
| A workshop-style content app that demonstrates wallet connection, Nitrolite session setup, | ||
| balance polling, and instant peer-to-peer support payments to post authors. | ||
| </p> | ||
| <div className="nitrolite-example-card__details"> | ||
| <div> | ||
| <h3>Functionality</h3> | ||
| <ul> | ||
| <li>MetaMask connection and Sepolia network selection.</li> | ||
| <li>Nitrolite client session with balance polling.</li> | ||
| <li>Small support transfers to author wallets.</li> | ||
| <li>YUSD and YELLOW asset selection.</li> | ||
| </ul> | ||
| </div> | ||
| <div> | ||
| <h3>Stack</h3> | ||
| <ul> | ||
| <li>Preact with Hooks, TypeScript, and Vite.</li> | ||
| <li>CSS Modules for app styling.</li> | ||
| <li>viem for wallet access and amount conversion.</li> | ||
| <li>Vercel-hosted static frontend.</li> | ||
| </ul> | ||
| </div> | ||
| <div> | ||
| <h3>SDKs</h3> | ||
| <ul> | ||
| <li><code>@yellow-org/sdk</code></li> | ||
| <li><code>@yellow-org/sdk-compat</code></li> | ||
| </ul> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div className="card__footer"> | ||
| <a className="button button--primary button--sm margin-right--sm" href="https://nexus-testnet-sepolia-ten.vercel.app/" target="_blank" rel="noopener noreferrer">Live demo</a> | ||
| <a className="button button--secondary button--sm" href="https://github.com/erc7824/nitrolite-example/tree/25c857184ad93a7319074546a985a510a9c0fefc" target="_blank" rel="noopener noreferrer">Source</a> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| --- | ||
| title: "Errors and recovery (compat)" | ||
| description: Typed compat errors and recovery recipes for migrated apps | ||
| sidebar_position: 6 | ||
| --- | ||
|
|
||
| # Errors and Recovery | ||
|
|
||
| At app boundaries, classify raw wallet or SDK failures and show only UI-safe copy: | ||
|
|
||
| ```typescript | ||
| import { NitroliteClient, getUserFacingMessage } from '@yellow-org/sdk-compat'; | ||
|
|
||
| try { | ||
| await client.deposit(tokenAddress, 11_000_000n); | ||
| } catch (err) { | ||
| const typed = NitroliteClient.classifyError(err); | ||
| showToast(getUserFacingMessage(typed)); | ||
| throw typed; | ||
| } | ||
| ``` | ||
|
|
||
| ## Error Catalogue | ||
|
|
||
| | Error class | Code | Trigger | Recovery | | ||
| |---|---|---|---| | ||
| | `CompatError` | varies | Base compat failure | Log `error.code`; show `getUserFacingMessage(error)` | | ||
| | `AllowanceError` | `ALLOWANCE_INSUFFICIENT` | ChannelHub needs ERC-20 allowance | Approve, then retry | | ||
| | `UserRejectedError` | `USER_REJECTED` | Wallet rejected a signature or transaction | Show retry UI; do not auto-loop prompts | | ||
| | `InsufficientFundsError` | `INSUFFICIENT_FUNDS` | Not enough gas or token balance | Show funding guidance | | ||
| | `NotInitializedError` | `NOT_INITIALIZED` | Wallet/client is disconnected | Reconnect and recreate the client | | ||
| | `OngoingStateTransitionError` | `ONGOING_STATE_TRANSITION` | Previous transition still finalizing | Poll status, then retry | | ||
|
|
||
| ## Allowance | ||
|
|
||
| Compat token helpers use token addresses and raw token units: | ||
|
|
||
| ```typescript | ||
| import { AllowanceError, NitroliteClient, getUserFacingMessage } from '@yellow-org/sdk-compat'; | ||
|
|
||
| try { | ||
| await client.deposit(tokenAddress, 11_000_000n); | ||
| } catch (err) { | ||
| const typed = NitroliteClient.classifyError(err); | ||
| showToast(getUserFacingMessage(typed)); | ||
|
|
||
| if (typed instanceof AllowanceError) { | ||
| await client.approveTokens(tokenAddress, 11_000_000n); | ||
| await client.deposit(tokenAddress, 11_000_000n); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| When you fall through to native v1, use `client.innerClient.approveToken(chainId, asset, amount)` with `Decimal`: | ||
|
|
||
| ```typescript | ||
| import Decimal from 'decimal.js'; | ||
| import { getUserFacingMessage } from '@yellow-org/sdk-compat'; | ||
|
|
||
| try { | ||
| await client.innerClient.checkpoint('usdc'); | ||
| } catch (err) { | ||
| showToast(getUserFacingMessage(err)); | ||
| // Native v1 approveToken expects a bigint chain ID. | ||
| await client.innerClient.approveToken(11155111n, 'usdc', new Decimal(11)); | ||
|
nksazonov marked this conversation as resolved.
|
||
| await client.innerClient.checkpoint('usdc'); | ||
| } | ||
| ``` | ||
|
|
||
| ## Prompt and Reconnect | ||
|
|
||
| For `UserRejectedError`, show `getUserFacingMessage(error)` and expose an explicit retry button. Do not auto-loop wallet prompts. For `NotInitializedError`, reconnect the wallet and recreate `NitroliteClient` before retrying. | ||
|
|
||
| ## Ongoing Transition | ||
|
|
||
| Use `EventPoller` to wait for channel status to settle before enabling another write: | ||
|
|
||
| ```typescript | ||
| import { | ||
| EventPoller, | ||
| OngoingStateTransitionError, | ||
| NitroliteClient, | ||
| RPCChannelStatus, | ||
| getUserFacingMessage, | ||
| } from '@yellow-org/sdk-compat'; | ||
|
|
||
| try { | ||
| await client.deposit(tokenAddress, 11_000_000n); | ||
| } catch (err) { | ||
| const typed = NitroliteClient.classifyError(err); | ||
| showToast(getUserFacingMessage(typed)); | ||
|
|
||
| if (typed instanceof OngoingStateTransitionError) { | ||
| const poller = new EventPoller(client, { | ||
| onChannelUpdate: (channels) => { | ||
| if (!channels.some((channel) => channel.status === RPCChannelStatus.Resizing)) { | ||
| poller.stop(); | ||
| enableRetry(); | ||
| } | ||
| }, | ||
| onError: (pollError) => showToast(getUserFacingMessage(pollError)), | ||
| }, 3000); | ||
| poller.start(); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Insufficient Funds | ||
|
|
||
| ```typescript | ||
| import { InsufficientFundsError, NitroliteClient, getUserFacingMessage } from '@yellow-org/sdk-compat'; | ||
|
|
||
| try { | ||
| await client.deposit(tokenAddress, 11_000_000n); | ||
| } catch (err) { | ||
| const typed = NitroliteClient.classifyError(err); | ||
| showToast(getUserFacingMessage(typed)); | ||
|
|
||
| if (typed instanceof InsufficientFundsError) { | ||
| const tokenBalance = await client.getTokenBalance(tokenAddress); | ||
|
nksazonov marked this conversation as resolved.
|
||
| renderFundingHelp({ tokenBalance, chainId: 11155111 }); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Missing RPC URL | ||
|
|
||
| On-chain helpers throw `No RPC URL configured for chain ...` when `blockchainRPCs` is missing. Fix the client config with `blockchainRPCs: { 80002: process.env.POLYGON_AMOY_RPC_URL! }`. | ||
|
|
||
| ```typescript | ||
| import { NitroliteClient, getUserFacingMessage } from '@yellow-org/sdk-compat'; | ||
|
|
||
| try { | ||
| await client.getOpenChannels(); | ||
|
nksazonov marked this conversation as resolved.
|
||
| } catch (err) { | ||
| const typed = NitroliteClient.classifyError(err); | ||
| showToast(getUserFacingMessage(typed)); | ||
| if (err instanceof Error && err.message.includes('No RPC URL configured')) promptForRpcUrl(); | ||
| } | ||
| ``` | ||
|
|
||
| ## Unsupported Asset or Wrong Amount Unit | ||
|
|
||
| ```typescript | ||
| try { | ||
| await client.transfer(destination, [{ asset: 'usdc', amount: '5000000' }]); | ||
| } catch (err) { | ||
| showToast(getUserFacingMessage(err)); | ||
|
|
||
| if (err instanceof Error && err.message.includes('Unknown asset')) { | ||
| renderSupportedAssets(await client.getAssetsList()); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| If a transfer uses `5.0`, fix it before retrying. Compat transfer uses raw asset-unit strings. Keep the [amount-units table](./overview#amount-units) next to migrated payment code reviews. | ||
|
|
||
| ## Failed Checkpoint | ||
|
|
||
| Retry with capped backoff only after approval and state preconditions are satisfied. Keep the visible message behind `getUserFacingMessage(err)`: | ||
|
|
||
| ```typescript | ||
| import { NitroliteClient, getUserFacingMessage } from '@yellow-org/sdk-compat'; | ||
|
|
||
| let lastError: unknown; | ||
|
|
||
| for (const waitMs of [1000, 3000, 5000]) { | ||
| try { | ||
| await client.innerClient.checkpoint('usdc'); | ||
| lastError = undefined; | ||
| break; | ||
| } catch (err) { | ||
| const typed = NitroliteClient.classifyError(err); | ||
| lastError = typed; | ||
| showToast(getUserFacingMessage(typed)); | ||
| await new Promise((resolve) => setTimeout(resolve, waitMs)); | ||
| } | ||
| } | ||
|
|
||
| if (lastError) { | ||
| throw lastError; | ||
| } | ||
| ``` | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.