diff --git a/docs/AGENTS.md b/docs/AGENTS.md index a696801d9..27558067e 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -80,7 +80,7 @@ npx skills add base/base-skills |base-chain/api-reference/ethereum-json-rpc-api:eth_blockNumber,eth_call,eth_chainId,eth_estimateGas,eth_feeHistory,eth_gasPrice,eth_getBalance,eth_getBlockByHash,eth_getBlockByNumber,eth_getBlockReceipts,eth_getBlockTransactionCountByHash,eth_getBlockTransactionCountByNumber,eth_getCode,eth_getLogs,eth_getStorageAt,eth_getTransactionByBlockHashAndIndex,eth_getTransactionByBlockNumberAndIndex,eth_getTransactionByHash,eth_getTransactionCount,eth_getTransactionReceipt,eth_maxPriorityFeePerGas,eth_sendRawTransaction,eth_subscribe,eth_syncing,eth_unsubscribe,net_version,web3_clientVersion |base-chain/api-reference/flashblocks-api:base_transactionStatus,eth_simulateV1,flashblocks-api-overview,newFlashblockTransactions,newFlashblocks,pendingLogs |base-chain/flashblocks:faq -|base-chain/network-information:base-contracts,base-solana-bridge,bridging-and-withdrawals,ecosystem-bridges,network-faucets,network-fees,throughput-and-limits,transaction-finality,transaction-ordering,troubleshooting-transactions +|base-chain/network-information:base-contracts,base-solana-bridge,bridging-and-withdrawals,configuration-changelog,ecosystem-bridges,network-faucets,network-fees,throughput-and-limits,transaction-finality,transaction-ordering,troubleshooting-transactions |base-chain/node-operators:node-providers,performance-tuning,run-a-base-node,snapshots,troubleshooting |base-chain/quickstart:connecting-to-base |base-chain/security:avoid-malicious-flags,bug-bounty,report-vulnerability,security-council @@ -93,7 +93,7 @@ npx skills add base/base-skills |base-chain/specs/protocol/proofs:challenger,contracts,index,proposer,registrar,tee-prover,zk-prover |base-chain/specs/reference:configurability,glossary |base-chain/specs/upgrades/azul:exec-engine,node-upgrade,overview,proofs -|base-chain/specs/upgrades/beryl:b20,overview +|base-chain/specs/upgrades/beryl:b20-playground,b20,overview |base-chain/specs/upgrades/canyon:overview |base-chain/specs/upgrades/delta:overview,span-batches |base-chain/specs/upgrades/ecotone:derivation,l1-attributes,overview diff --git a/docs/apps/guides/accept-b20-payments.mdx b/docs/apps/guides/accept-b20-payments.mdx index 6576467d6..88a14ac0e 100644 --- a/docs/apps/guides/accept-b20-payments.mdx +++ b/docs/apps/guides/accept-b20-payments.mdx @@ -58,4 +58,4 @@ Call `publicClient.simulateContract` with the same arguments before sending. It - [B20 token standard](/base-chain/specs/upgrades/beryl/b20): the full interface, including memos, policies, pausing, and roles. - [Query B20 events](https://docs.cdp.coinbase.com/data/sql-api/b20-events): index `Transfer` and `Memo` events with the CDP SQL API to reconcile payments against orders at scale. -- [Launch a B20 Token](/get-started/launch-b20-token): create your own B20 token. \ No newline at end of file +- [Launch a B20 token](/get-started/launch-b20-token): create your own B20 token. \ No newline at end of file diff --git a/docs/base-chain/specs/upgrades/beryl/b20-playground.mdx b/docs/base-chain/specs/upgrades/beryl/b20-playground.mdx new file mode 100644 index 000000000..88d70ca4d --- /dev/null +++ b/docs/base-chain/specs/upgrades/beryl/b20-playground.mdx @@ -0,0 +1,64 @@ +--- +title: "B20 playground" +description: "Try B20's issuer flows in your browser: freeze-and-seize, payment memos, gasless approvals, and onchain corporate actions on Base." +--- + +import { B20FlowDemo } from "/snippets/B20PlaygroundDemo.jsx" + + +## When to use B20 + +- You need compliance, reconciliation, or corporate-action machinery. It ships as part of the standard, so you don't build and audit a custom contract. +- Your integrators matter. Exchanges, wallets, and indexers wire up one surface that behaves the same across every B20 token. +- Cost matters. B20 runs as a native precompile: cheaper and faster than a contract token. + +B20 is a superset of ERC-20: every ERC-20 call works unchanged, so there's no compatibility trade-off in choosing it. + +## A compliance order arrives + +A court order targets one account. Freeze it so it can't send, then seize the balance with one call. Seizure only works on an account that is already frozen. Every other holder is untouched and the token keeps trading. See [transfer policies](/base-chain/specs/upgrades/beryl/b20#policy-integration) and [burn](/base-chain/specs/upgrades/beryl/b20#burn) in the spec. + + + + + +## A customer pays an invoice + +Your customer pays to one address. The transfer carries the invoice reference, so payment and order ID land in the same transaction. Reconciliation becomes a log query instead of a deposit address for every customer. To build this, see [Accept B20 payments](/apps/guides/accept-b20-payments). + + + + + +## A new user approves without gas + +A new user shouldn't have to buy ETH first. With `permit`, they sign an approval offchain and your platform relays it. They start transacting with zero gas, and nonces stop signature replay. See [permit](/base-chain/specs/upgrades/beryl/b20#erc-2612-permit--eip-712) in the spec. + + + + + +## The stock splits + +A 2-for-1 split is one call. Every balance doubles without a migration or a new contract. Dividends run inside a public announcement window, so the disclosure lives onchain next to the action. The token also stores identifiers like a CUSIP. See the [Asset variant](/base-chain/specs/upgrades/beryl/b20#asset) in the spec. + + + + + +## Build with it + + + + Deploy a fully configured token in one factory call. + + + Integration code for memo-tagged payments and B20-specific reverts. + + + Every method, event, role, and policy in the standard. + + + The upgrade that introduces B20 and native token features. + + diff --git a/docs/base-chain/specs/upgrades/beryl/b20.mdx b/docs/base-chain/specs/upgrades/beryl/b20.mdx index eca60a93a..2daaf3aa5 100644 --- a/docs/base-chain/specs/upgrades/beryl/b20.mdx +++ b/docs/base-chain/specs/upgrades/beryl/b20.mdx @@ -1,11 +1,11 @@ --- -title: "B20 Native Token Standard" -description: "B20 is Base's native token standard - designed for stablecoin issuers, real-world asset (RWA) and equity issuers, and long-tail token creators." +title: "B20 native token standard" +description: "Learn how B20, Base's native token standard, serves stablecoin issuers, real-world asset (RWA) and equity issuers, and long-tail token creators." --- B20 is the Base ecosystem's own version of [ERC-20](https://eips.ethereum.org/EIPS/eip-20). It ships with a built-in compliance toolkit: transfer policies, freeze-and-seize, role-based access control, memos, and supply caps. The full interface specs are available in the [Base Standard Library](https://github.com/base/base-std/tree/main) repository. -To deploy your first B20 token, see the [Launch a B20 Token](/get-started/launch-b20-token) quickstart. +To deploy your first B20 token, see the [Launch a B20 token](/get-started/launch-b20-token) quickstart. [Verify the Activation Registry is enabled](/get-started/launch-b20-token#verify-the-activation-registry-is-enabled) before attempting to deploy. diff --git a/docs/docs.json b/docs/docs.json index 10d4e9b06..9387e4ea1 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -102,7 +102,8 @@ "group": "Beryl Upgrade", "tag": "New", "pages": [ - "base-chain/specs/upgrades/beryl/overview" + "base-chain/specs/upgrades/beryl/overview", + "base-chain/specs/upgrades/beryl/b20-playground" ] }, { diff --git a/docs/get-started/launch-b20-token.mdx b/docs/get-started/launch-b20-token.mdx index c0f61578a..e1880ae81 100644 --- a/docs/get-started/launch-b20-token.mdx +++ b/docs/get-started/launch-b20-token.mdx @@ -1,6 +1,6 @@ --- -title: "Launch a B20 Token" -description: "Launch a B20 token on Base by calling the B20 Factory precompile." +title: "Launch a B20 token" +description: "Launch a B20 token on Base with one call to the B20 Factory precompile, with roles, supply caps, and compliance controls built in." --- B20 is an ERC-20 superset that runs as a native precompile on Base, which makes transfers cheaper and higher-throughput than a standard contract token while keeping full ERC-20 compatibility. Roles, supply caps, pausing, policy gating, memos, and `permit` are built into the chain. diff --git a/docs/snippets/B20PlaygroundDemo.jsx b/docs/snippets/B20PlaygroundDemo.jsx new file mode 100644 index 000000000..4502cd70e --- /dev/null +++ b/docs/snippets/B20PlaygroundDemo.jsx @@ -0,0 +1,497 @@ + +export const B20FlowDemo = ({ flow }) => { + // Base brand: Base Sans / Base Mono with brand-recommended fallbacks. + const sans = "'Base Sans','Inter Tight',Inter,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif"; + const mono = "'Base Mono','Roboto Mono',ui-monospace,'SF Mono',Menlo,Consolas,monospace"; + + // Palette resolves through CSS variables set on .b20f-card (light) and + // overridden under html.dark, so the widget tracks the site theme. + const c = { + bg: "var(--b20-bg)", panel: "var(--b20-panel)", border: "var(--b20-border)", + text: "var(--b20-text)", body: "var(--b20-body)", muted: "var(--b20-muted)", dim: "var(--b20-dim)", + accent: "var(--b20-accent)", accentContrast: "var(--b20-accent-contrast)", + success: "var(--b20-success)", error: "var(--b20-error)", + }; + + // Base secondary palette for account markers. + const acctDot = { Issuer: "var(--b20-accent)", Processor: "#3c8aff", Alice: "#66c800", Bob: "#ffd12f", Carol: "#fea8cd" }; + + // ----- deterministic simulated address ----- + const hashHex = (seed) => { + let h = 5381; + for (let i = 0; i < seed.length; i++) h = ((h << 5) + h + seed.charCodeAt(i)) >>> 0; + let out = "", x = h; + for (let i = 0; i < 9; i++) { + x = (x * 1103515245 + 12345 + i * 7) >>> 0; + out += ((x >>> ((i % 4) * 8)) & 0xff).toString(16).padStart(2, "0"); + } + return out; + }; + const tokenAddress = (variant, symbol) => { + const vByte = variant === "STABLECOIN" ? "01" : "00"; + const tail = hashHex(symbol); + return `0xb2000000…${vByte}${tail.slice(0, 4)}…${tail.slice(-4)}`; + }; + + const emit = (name, args, logIndex) => ({ kind: "event", name, args, logIndex }); + const revert = (name, args) => ({ kind: "revert", name, args: args || "", logIndex: null }); + const info = (name, args) => ({ kind: "info", name, args: args || "", logIndex: null }); + + // ----- fresh simulation state ----- + const freshSim = () => ({ + token: null, balances: {}, policies: {}, nextPolicyId: 2, + scopes: { TRANSFER_SENDER: 0, TRANSFER_RECEIVER: 0, TRANSFER_EXECUTOR: 0, MINT_RECEIVER: 0 }, + roles: {}, allowances: {}, nonces: {}, sig: null, + }); + const cloneSim = (s) => ({ + token: s.token ? { ...s.token, extraMetadata: { ...s.token.extraMetadata } } : null, + balances: { ...s.balances }, + policies: Object.fromEntries(Object.entries(s.policies).map(([k, v]) => [k, { ...v, members: [...v.members] }])), + nextPolicyId: s.nextPolicyId, + scopes: { ...s.scopes }, + roles: Object.fromEntries(Object.entries(s.roles).map(([k, v]) => [k, [...v]])), + allowances: { ...s.allowances }, + nonces: { ...s.nonces }, + sig: s.sig ? { ...s.sig } : null, + }); + + // Provision a token directly into the snapshot (simulated createB20). + const createInSim = (s, kind) => { + const isEquity = kind === "EQUITY"; + s.token = { + variant: isEquity ? "ASSET" : "STABLECOIN", + name: isEquity ? "Acme Corp Class A" : "Acme Dollar", + symbol: isEquity ? "ACME" : "aUSD", + multiplier: 1.0, + extraMetadata: isEquity ? { cusip: "38259P508" } : {}, + }; + const roleMap = {}; + ["DEFAULT_ADMIN_ROLE", "MINT_ROLE", "BURN_BLOCKED_ROLE"].forEach(r => { roleMap[r] = ["Issuer"]; }); + if (isEquity) roleMap["OPERATOR_ROLE"] = ["Issuer"]; + s.roles = roleMap; + }; + + // --------------------------------------------------------------- + // Op interpreter. Mutates the snapshot; validates in spec order; + // returns log entries for the step's inline result. + // --------------------------------------------------------------- + const applyOp = (s, op, me, entries) => { + const isAuthorized = (policyId, account) => { + if (!policyId) return true; // ALWAYS_ALLOW = 0, the default on every scope + const p = s.policies[policyId]; + if (!p) return false; + if (p.type === "BLOCKLIST") return !p.members.includes(account); + return p.members.includes(account); // ALLOWLIST + }; + const roleHas = (r, a) => (s.roles[r] || []).includes(a); + + switch (op.type) { + case "mint": { + if (!roleHas("MINT_ROLE", me)) return entries.push(revert("AccessControlUnauthorizedAccount", `${me} lacks MINT_ROLE`)); + if (!isAuthorized(s.scopes.MINT_RECEIVER, op.to)) return entries.push(revert("PolicyForbids", `MINT_RECEIVER · ${op.to}`)); + s.balances[op.to] = (s.balances[op.to] || 0) + op.amt; + return entries.push(emit("Transfer", `0x0 → ${op.to} · ${op.amt}`, 0)); + } + case "transfer": { + const { from, to, amt, memo } = op; + if (!isAuthorized(s.scopes.TRANSFER_SENDER, from)) return entries.push(revert("PolicyForbids", `TRANSFER_SENDER · ${from}`)); + if (!isAuthorized(s.scopes.TRANSFER_RECEIVER, to)) return entries.push(revert("PolicyForbids", `TRANSFER_RECEIVER · ${to}`)); + if ((s.balances[from] || 0) < amt) return entries.push(revert("ERC20InsufficientBalance", `${from} has ${s.balances[from] || 0}`)); + s.balances[from] -= amt; + s.balances[to] = (s.balances[to] || 0) + amt; + entries.push(emit("Transfer", `${from} → ${to} · ${amt}`, 0)); + if (memo !== undefined) entries.push(emit("Memo", `${me} · "${memo}"`, 1)); + return; + } + case "transferFrom": { + const { from, to, amt } = op; + const key = from + "→" + me; + if ((s.allowances[key] || 0) < amt) return entries.push(revert("ERC20InsufficientAllowance", `${me} allowance ${s.allowances[key] || 0} < ${amt}`)); + if ((s.balances[from] || 0) < amt) return entries.push(revert("ERC20InsufficientBalance", `${from} has ${s.balances[from] || 0}`)); + s.balances[from] -= amt; + s.balances[to] = (s.balances[to] || 0) + amt; + s.allowances[key] -= amt; + return entries.push(emit("Transfer", `${from} → ${to} · ${amt} (by ${me})`, 0)); + } + case "burnBlocked": { + const { target, amt } = op; + if (!roleHas("BURN_BLOCKED_ROLE", me)) return entries.push(revert("AccessControlUnauthorizedAccount", `${me} lacks BURN_BLOCKED_ROLE`)); + if (isAuthorized(s.scopes.TRANSFER_SENDER, target)) return entries.push(revert("AccountNotBlocked", `${target} not denied by TRANSFER_SENDER`)); + if ((s.balances[target] || 0) < amt) return entries.push(revert("ERC20InsufficientBalance", `${target} has ${s.balances[target] || 0}`)); + s.balances[target] -= amt; + return entries.push(emit("Transfer", `${target} → 0x0 · ${amt} (seized)`, 0)); + } + case "freeze": { + // createPolicy + updateBlocklist + updatePolicy, presented as one action + const id = s.nextPolicyId; + s.policies[id] = { type: "BLOCKLIST", members: [op.target] }; + s.nextPolicyId = id + 1; + s.scopes.TRANSFER_SENDER = id; + entries.push(emit("PolicyCreated", `#${id} · BLOCKLIST · admin ${me}`, 0)); + entries.push(emit("updateBlocklist", `#${id} · add ${op.target}`, 1)); + entries.push(emit("PolicyUpdated", `TRANSFER_SENDER → #${id}`, 2)); + return; + } + case "permit": { + const { owner, spender, amt, nonce } = op; + const current = s.nonces[owner] || 0; + if (nonce !== current) return entries.push(revert("ERC2612InvalidSigner", `stale nonce ${nonce} ≠ ${current}`)); + s.allowances[owner + "→" + spender] = amt; + s.nonces[owner] = current + 1; + return entries.push(emit("Approval", `${owner} → ${spender} · ${amt} (permit, relayed by ${me})`, 0)); + } + case "updateMultiplier": { + if (!roleHas("OPERATOR_ROLE", me)) return entries.push(revert("AccessControlUnauthorizedAccount", `${me} lacks OPERATOR_ROLE`)); + s.token = { ...s.token, multiplier: op.value }; + return entries.push(emit("MultiplierUpdated", `${op.value}× (WAD)`, 0)); + } + case "announceBatchMint": { + const { recipients, amt, id } = op; + if (!roleHas("OPERATOR_ROLE", me)) return entries.push(revert("AccessControlUnauthorizedAccount", `${me} lacks OPERATOR_ROLE`)); + entries.push(emit("Announcement", `id ${id} · dividend batchMint`, 0)); + recipients.forEach((r, i) => { + s.balances[r] = (s.balances[r] || 0) + amt; + entries.push(emit("Transfer", `0x0 → ${r} · ${amt}`, i + 1)); + }); + entries.push(emit("EndAnnouncement", `id ${id}`, recipients.length + 1)); + return; + } + default: + return; + } + }; + + const runOps = (s, ops) => { + const entries = []; + ops.forEach(op => applyOp(s, op, op.as, entries)); + return entries; + }; + + // ====================================================================== + // Flow definitions. Each step: plain-language situation → one button → + // visible consequence. API names appear in results, never in the ask. + // ====================================================================== + const FLOWS = { + seize: { + title: "Freeze and seize, as compliance sees it", + erc20: "On plain ERC-20: you build and audit a custom blocklist token.", + readout: "balances", + steps: [ + { + label: "Bob holds 50 aUSD.", + action: "Mint to Bob", + run: (s) => { + if (!s.token) createInSim(s, "STABLECOIN"); + return { entries: runOps(s, [{ as: "Issuer", type: "mint", to: "Bob", amt: 50 }]) }; + }, + }, + { + label: "A court order arrives. Freeze Bob's account.", + action: "Freeze account", + run: (s) => ({ + entries: runOps(s, [{ as: "Issuer", type: "freeze", target: "Bob" }]), + caption: "Bob can no longer send. No one else is affected.", + }), + }, + { + label: "Bob tries to pay Alice anyway.", + action: "Attempt payment", + run: (s) => ({ + entries: runOps(s, [{ as: "Bob", type: "transfer", from: "Bob", to: "Alice", amt: 10 }]), + caption: "Blocked by the protocol, not by custom contract code.", + }), + }, + { + label: "Seize the frozen balance.", + action: "Seize funds", + run: (s) => ({ + entries: runOps(s, [{ as: "Issuer", type: "burnBlocked", target: "Bob", amt: 50 }]), + caption: "Seizure only works on a frozen account. It can't skip the freeze.", + }), + }, + ], + }, + + memo: { + title: "An invoice paid and matched", + erc20: "On plain ERC-20: transfers carry no reference, so you run a deposit address per customer.", + readout: null, + steps: [ + { + label: "Alice has 100 aUSD to spend.", + action: "Fund Alice", + run: (s) => { + if (!s.token) createInSim(s, "STABLECOIN"); + return { entries: runOps(s, [{ as: "Issuer", type: "mint", to: "Alice", amt: 100 }]) }; + }, + }, + { + label: "Alice pays invoice #8842. The reference rides in the payment.", + action: "Pay 25 aUSD", + run: (s) => ({ + entries: runOps(s, [{ as: "Alice", type: "transfer", from: "Alice", to: "Processor", amt: 25, memo: "invoice-8842" }]), + caption: "One transaction: payment and reference.", + }), + }, + { + label: "The back office matches the payment to the order.", + action: "Match payment", + run: () => ({ + entries: [ + info("query", "find Transfer at (txHash, Memo.logIndex − 1)"), + info("matched", '"invoice-8842" → 25 aUSD from Alice ✓'), + ], + caption: "Reconciliation is one log query, not a deposit address per customer.", + }), + }, + ], + }, + + permit: { + title: "A first-time user, zero ETH", + erc20: "On plain ERC-20: the user buys ETH and sends an onchain approve first.", + readout: "allowance", + steps: [ + { + label: "Alice just signed up. She has aUSD and zero ETH for gas.", + action: "Fund with aUSD", + run: (s) => { + if (!s.token) createInSim(s, "STABLECOIN"); + return { entries: runOps(s, [{ as: "Issuer", type: "mint", to: "Alice", amt: 100 }]) }; + }, + }, + { + label: "Alice signs an approval offchain. It costs her nothing.", + action: "Sign approval", + run: (s) => { + s.sig = { nonce: s.nonces["Alice"] || 0 }; + return { + entries: [ + info("signed payload", `owner Alice · spender Processor · value 100 · nonce ${s.sig.nonce}`), + info("cost to Alice", "0 gas, nothing sent onchain"), + ], + }; + }, + }, + { + label: "The platform relays her signature and collects the payment.", + action: "Relay & collect", + run: (s) => ({ + entries: runOps(s, [ + { as: "Processor", type: "permit", owner: "Alice", spender: "Processor", amt: 100, nonce: s.sig ? s.sig.nonce : 0 }, + { as: "Processor", type: "transferFrom", from: "Alice", to: "Processor", amt: 40 }, + ]), + caption: "Alice never sent a transaction. The nonce blocks replay.", + }), + }, + ], + }, + + equity: { + title: "A share of stock, onchain", + erc20: "On plain ERC-20: a rebasing token is a custom contract, and disclosures live offchain.", + readout: "scaled", + steps: [ + { + label: "ACME lists onchain with its security identifiers.", + action: "Create token", + run: (s) => { + createInSim(s, "EQUITY"); + return { + entries: [ + info("createB20", `asset · ACME · ${tokenAddress("ASSET", "ACME")}`), + info("initCalls", 'updateExtraMetadata("cusip", "38259P508")'), + ], + caption: "One factory call, with no contract to write or audit.", + }; + }, + }, + { + label: "Shareholders hold ACME.", + action: "Distribute shares", + run: (s) => ({ + entries: runOps(s, [ + { as: "Issuer", type: "mint", to: "Alice", amt: 100 }, + { as: "Issuer", type: "mint", to: "Bob", amt: 50 }, + ]), + }), + }, + { + label: "The board declares a 2-for-1 split.", + action: "Run the split", + run: (s) => ({ + entries: runOps(s, [{ as: "Issuer", type: "updateMultiplier", value: 2.0 }]), + caption: "Every balance doubles in one call, without a migration or a new contract.", + }), + }, + { + label: "A dividend goes out with public disclosure.", + action: "Announce & distribute", + run: (s) => ({ + entries: runOps(s, [{ as: "Issuer", type: "announceBatchMint", recipients: ["Alice", "Bob"], amt: 10, id: 7 }]), + caption: "Disclosure and distribution land in the same transaction.", + }), + }, + ], + }, + }; + + const f = FLOWS[flow] || FLOWS.seize; + + // ----- widget state ----- + const [sim, setSim] = useState(freshSim); + const [results, setResults] = useState([]); // one entry per completed step + const stepIndex = results.length; + const done = stepIndex >= f.steps.length; + + const runStep = () => { + if (done) return; + const s = cloneSim(sim); + const out = f.steps[stepIndex].run(s); + setSim(s); + setResults(r => [...r, out]); + }; + const reset = () => { setSim(freshSim()); setResults([]); }; + + // ----- flow-specific live readout ----- + const Readout = () => { + if (!f.readout || !sim.token) return null; + if (f.readout === "allowance") { + return ( +
+ allowance(Alice → Processor) {sim.allowances["Alice→Processor"] || 0} + nonce(Alice) {sim.nonces["Alice"] || 0} +
+ ); + } + const scaled = f.readout === "scaled"; + const frozen = f.readout === "balances" && sim.scopes.TRANSFER_SENDER !== 0; + const holders = Object.keys(sim.balances).length ? Object.keys(sim.balances) : []; + if (!holders.length) return null; + return ( +
+ {holders.map(a => ( + + + {a} {sim.balances[a] || 0} + {scaled && sim.token.multiplier !== 1 && ( + → {((sim.balances[a] || 0) * sim.token.multiplier).toFixed(0)} scaled + )} + {frozen && a === "Bob" && ( + frozen + )} + + ))} + {scaled && sim.token.multiplier !== 1 && ( + multiplier() = {sim.token.multiplier}× + )} +
+ ); + }; + + return ( +
+ + + {/* Header */} +
+ {f.title} + SIMULATED +
+ {results.length > 0 && } +
+ + {/* Steps */} +
+ {f.steps.map((step, i) => { + const state = i < stepIndex ? "done" : i === stepIndex ? "now" : "future"; + const res = results[i]; + return ( +
+
+ + {state === "done" ? "✓" : i + 1} + + + {step.label} + + {state !== "done" && ( + + )} +
+ {res && ( +
+ {res.entries.map((e, j) => ( +
+ + {e.kind === "event" && } + {e.kind === "revert" && } + {e.kind === "info" && } + + + {e.logIndex !== null && e.logIndex !== undefined && [{e.logIndex}] } + {e.name} + {e.args && · {e.args}} + +
+ ))} + {res.caption && ( +
{res.caption}
+ )} +
+ )} +
+ ); + })} + + {/* Live readout */} + {(f.readout && Object.keys(sim.balances).length > 0) && ( +
+ +
+ )} +
+ + {/* Footer */} +
+ {f.erc20} +
+ {done && ✓ flow complete} +
+
+ ); +}; diff --git a/package-lock.json b/package-lock.json index ae14091fc..6ab682576 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "base-docs", + "name": "docs", "lockfileVersion": 3, "requires": true, "packages": {}