Repository: onflow/flow-js-sdk Branch: master Commit: 5e44c67ea09d Files: 1103 Total size: 5.1 MB Directory structure: gitextract_j_zi96gh/ ├── .changeset/ │ ├── README.md │ └── config.json ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── scripts/ │ │ └── prevent-major-bumps.js │ └── workflows/ │ ├── add-pitches-to-project.yml │ ├── changeset-check.yml │ ├── code-analysis.yml │ ├── dependancy-review.yml │ ├── integrate.yml │ ├── promote-playground.yml │ └── release.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── AGENTS.md ├── CHANGELOG.md ├── CITATION.cff ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── SECURITY.md ├── docs/ │ ├── flow-docs.json │ ├── index.md │ ├── index.mdx │ ├── reference/ │ │ ├── api.md │ │ ├── authentication.mdx │ │ ├── configure-fcl.mdx │ │ ├── discovery.mdx │ │ ├── installation.mdx │ │ ├── interaction-templates.mdx │ │ ├── proving-authentication.mdx │ │ ├── scripts.mdx │ │ ├── sdk-guidelines.mdx │ │ ├── transactions.mdx │ │ ├── user-signatures.mdx │ │ └── wallet-connect.mdx │ └── tutorials/ │ └── flow-app-quickstart.mdx ├── docs-generator/ │ ├── README.md │ ├── generate-all-docs.js │ ├── generate-docs.js │ ├── generators/ │ │ ├── generate-function-page.js │ │ ├── generate-namespace-page.js │ │ ├── generate-package-page.js │ │ ├── generate-root-page.js │ │ ├── generate-types-page.js │ │ ├── index.js │ │ └── utils/ │ │ ├── extract-utils.js │ │ ├── generate-page.js │ │ ├── index.js │ │ └── parse-config-custom-data.js │ ├── templates/ │ │ ├── function.hbs │ │ ├── namespace.hbs │ │ ├── package.hbs │ │ ├── root.hbs │ │ └── types.hbs │ └── utils/ │ ├── export-extractor.js │ ├── file-utils.js │ ├── function-extractor.js │ ├── index.js │ ├── jsdoc-parser.js │ ├── namespace-utils.js │ ├── type-utils.js │ └── typescript-formatter.js ├── jest.config.js ├── lerna.json ├── package.json ├── packages/ │ ├── config/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .eslintrc.json │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── config.test.ts │ │ │ ├── config.ts │ │ │ └── utils/ │ │ │ ├── utils.test.ts │ │ │ └── utils.ts │ │ └── tsconfig.json │ ├── demo/ │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── DEPLOY.md │ │ ├── README.md │ │ ├── emulator.key │ │ ├── eslint.config.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── public/ │ │ │ ├── robots.txt │ │ │ └── sitemap.xml │ │ ├── src/ │ │ │ ├── app.css │ │ │ ├── app.tsx │ │ │ ├── components/ │ │ │ │ ├── advanced-cards/ │ │ │ │ │ ├── dark-mode-card.tsx │ │ │ │ │ └── theming-card.tsx │ │ │ │ ├── component-cards/ │ │ │ │ │ ├── connect-card.tsx │ │ │ │ │ ├── demo-nft-card.tsx │ │ │ │ │ ├── profile-card.tsx │ │ │ │ │ ├── scheduled-transaction-list-demo.tsx │ │ │ │ │ ├── transaction-button-card.tsx │ │ │ │ │ ├── transaction-dialog-card.tsx │ │ │ │ │ └── transaction-link-card.tsx │ │ │ │ ├── content-section.tsx │ │ │ │ ├── content-sidebar.tsx │ │ │ │ ├── flow-provider-wrapper.tsx │ │ │ │ ├── footer.tsx │ │ │ │ ├── header.tsx │ │ │ │ ├── hook-cards/ │ │ │ │ │ ├── use-cross-vm-bridge-nft-from-evm-card.tsx │ │ │ │ │ ├── use-cross-vm-bridge-nft-to-evm-card.tsx │ │ │ │ │ ├── use-cross-vm-bridge-token-from-evm-card.tsx │ │ │ │ │ ├── use-cross-vm-bridge-token-to-evm-card.tsx │ │ │ │ │ ├── use-flow-account-card.tsx │ │ │ │ │ ├── use-flow-block-card.tsx │ │ │ │ │ ├── use-flow-chain-id-card.tsx │ │ │ │ │ ├── use-flow-config-card.tsx │ │ │ │ │ ├── use-flow-current-user-card.tsx │ │ │ │ │ ├── use-flow-events-card.tsx │ │ │ │ │ ├── use-flow-mutate-card.tsx │ │ │ │ │ ├── use-flow-nft-metadata-card.tsx │ │ │ │ │ ├── use-flow-query-card.tsx │ │ │ │ │ ├── use-flow-query-raw-card.tsx │ │ │ │ │ ├── use-flow-revertible-random-card.tsx │ │ │ │ │ ├── use-flow-scheduled-transaction-card.tsx │ │ │ │ │ └── use-flow-transaction-status-card.tsx │ │ │ │ ├── setup-cards/ │ │ │ │ │ └── installation-card.tsx │ │ │ │ ├── starter-banner.tsx │ │ │ │ └── ui/ │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-viewer.tsx │ │ │ │ ├── demo-card.tsx │ │ │ │ ├── error-boundary.tsx │ │ │ │ ├── inline-code.tsx │ │ │ │ ├── json-viewer.tsx │ │ │ │ ├── loading-spinner.tsx │ │ │ │ ├── plus-grid.tsx │ │ │ │ ├── props-viewer.tsx │ │ │ │ └── results-section.tsx │ │ │ ├── constants.ts │ │ │ ├── main.tsx │ │ │ ├── utils/ │ │ │ │ └── chain-helpers.ts │ │ │ ├── utils.ts │ │ │ └── vite-env.d.ts │ │ ├── tailwind.config.js │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── fcl/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .eslintrc.json │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── TRANSITIONS.md │ │ ├── docs/ │ │ │ └── extra.md │ │ ├── docs-generator.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── VERSION.ts │ │ │ ├── client.ts │ │ │ ├── discovery/ │ │ │ │ ├── exec-discovery.ts │ │ │ │ ├── exec-hook.ts │ │ │ │ └── rpc/ │ │ │ │ ├── client.ts │ │ │ │ ├── handlers/ │ │ │ │ │ ├── exec-service.ts │ │ │ │ │ └── request-wc-qr.ts │ │ │ │ └── requests.ts │ │ │ ├── fcl.ts │ │ │ └── utils/ │ │ │ ├── async.ts │ │ │ ├── walletconnect/ │ │ │ │ └── loader.ts │ │ │ └── web/ │ │ │ ├── __tests__/ │ │ │ │ └── default-config.test.js │ │ │ ├── coreStrategies.js │ │ │ ├── default-config.js │ │ │ ├── exec-local.js │ │ │ ├── index.js │ │ │ ├── render-frame.js │ │ │ ├── render-pop.js │ │ │ ├── render-tab.js │ │ │ ├── storage.ts │ │ │ └── strategies/ │ │ │ ├── ext-rpc.js │ │ │ ├── iframe-rpc.js │ │ │ ├── pop-rpc.js │ │ │ ├── tab-rpc.js │ │ │ └── utils/ │ │ │ ├── extension.js │ │ │ ├── frame.js │ │ │ ├── pop.js │ │ │ └── tab.js │ │ └── tsconfig.json │ ├── fcl-bundle/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── build/ │ │ │ ├── build-watch.js │ │ │ ├── build.js │ │ │ ├── get-input-options.js │ │ │ ├── get-output-options.js │ │ │ └── watcher-pool.js │ │ ├── cli.js │ │ ├── get-package-json.js │ │ ├── package-config.js │ │ ├── plugins/ │ │ │ ├── banner.js │ │ │ └── preserve-dynamic-imports.js │ │ ├── program.js │ │ └── util.js │ ├── fcl-core/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .eslintrc.json │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── TRANSITIONS.md │ │ ├── WARNINGS.md │ │ ├── assets/ │ │ │ └── service-method-diagrams/ │ │ │ ├── http-post.excalidraw │ │ │ ├── iframe-rpc.excalidraw │ │ │ ├── pop-rpc.excalidraw │ │ │ └── tab-rpc.excalidraw │ │ ├── docs/ │ │ │ └── extra.md │ │ ├── docs-generator.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── VERSION.ts │ │ │ ├── app-utils/ │ │ │ │ ├── __tests__/ │ │ │ │ │ └── verify-user-sig.test.ts │ │ │ │ ├── index.ts │ │ │ │ └── verify-signatures.ts │ │ │ ├── client.ts │ │ │ ├── context/ │ │ │ │ ├── global.ts │ │ │ │ └── index.ts │ │ │ ├── current-user/ │ │ │ │ ├── build-user.ts │ │ │ │ ├── exec-service/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── plugins.ts │ │ │ │ │ ├── strategies/ │ │ │ │ │ │ ├── http-post.ts │ │ │ │ │ │ └── utils/ │ │ │ │ │ │ ├── buildMessageHandler.ts │ │ │ │ │ │ ├── fetch-service.ts │ │ │ │ │ │ ├── poll.ts │ │ │ │ │ │ └── service-endpoint.ts │ │ │ │ │ └── wc-check.ts │ │ │ │ ├── fetch-services.ts │ │ │ │ ├── index.ts │ │ │ │ ├── merge-services.ts │ │ │ │ ├── service-of-type.test.ts │ │ │ │ ├── service-of-type.ts │ │ │ │ └── url-from-service.ts │ │ │ ├── default-config.ts │ │ │ ├── discovery/ │ │ │ │ ├── index.ts │ │ │ │ ├── services/ │ │ │ │ │ └── authn.ts │ │ │ │ ├── services.test.ts │ │ │ │ ├── services.ts │ │ │ │ └── utils.ts │ │ │ ├── document/ │ │ │ │ ├── document.test.ts │ │ │ │ └── document.ts │ │ │ ├── events/ │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ └── legacy-events.ts │ │ │ ├── exec/ │ │ │ │ ├── args.ts │ │ │ │ ├── mutate.md │ │ │ │ ├── mutate.ts │ │ │ │ ├── query-raw.ts │ │ │ │ ├── query.md │ │ │ │ ├── query.ts │ │ │ │ ├── utils/ │ │ │ │ │ ├── normalize-args.ts │ │ │ │ │ ├── pre.ts │ │ │ │ │ ├── prep-template-opts.test.ts │ │ │ │ │ └── prep-template-opts.ts │ │ │ │ └── verify.ts │ │ │ ├── fcl-core.ts │ │ │ ├── fcl.test.ts │ │ │ ├── interaction-template-utils/ │ │ │ │ ├── derive-cadence-by-network/ │ │ │ │ │ ├── derive-cadence-by-network-1.0.0.ts │ │ │ │ │ ├── derive-cadence-by-network-1.1.0.ts │ │ │ │ │ ├── derive-cadence-by-network.test.ts │ │ │ │ │ └── derive-cadence-by-network.ts │ │ │ │ ├── generate-dependency-pin/ │ │ │ │ │ ├── generate-dependency-pin-1.0.0.ts │ │ │ │ │ ├── generate-dependency-pin-1.1.0.ts │ │ │ │ │ ├── generate-dependency-pin.test.ts │ │ │ │ │ └── generate-dependency-pin.ts │ │ │ │ ├── generate-template-id/ │ │ │ │ │ ├── generate-template-id-1.0.0.ts │ │ │ │ │ ├── generate-template-id-1.1.0.test.ts │ │ │ │ │ ├── generate-template-id-1.1.0.ts │ │ │ │ │ └── generate-template-id.ts │ │ │ │ ├── get-interaction-template-audits.ts │ │ │ │ ├── get-template-argument-message.test.ts │ │ │ │ ├── get-template-argument-message.ts │ │ │ │ ├── get-template-message.test.ts │ │ │ │ ├── get-template-message.ts │ │ │ │ ├── index.ts │ │ │ │ ├── interaction-template.ts │ │ │ │ ├── utils/ │ │ │ │ │ ├── find-imports.test.ts │ │ │ │ │ ├── find-imports.ts │ │ │ │ │ ├── generate-import.ts │ │ │ │ │ ├── hash.ts │ │ │ │ │ ├── replace-string-imports.test.ts │ │ │ │ │ └── replace-string-imports.ts │ │ │ │ ├── verify-dependency-pin-same-at-block.test.ts │ │ │ │ └── verify-dependency-pin-same-at-block.ts │ │ │ ├── normalizers/ │ │ │ │ └── service/ │ │ │ │ ├── __vsn.ts │ │ │ │ ├── account-proof.ts │ │ │ │ ├── authn-refresh.ts │ │ │ │ ├── authn.ts │ │ │ │ ├── authz.ts │ │ │ │ ├── back-channel-rpc.ts │ │ │ │ ├── composite-signature.ts │ │ │ │ ├── frame.ts │ │ │ │ ├── local-view.ts │ │ │ │ ├── open-id.ts │ │ │ │ ├── polling-response.ts │ │ │ │ ├── pre-authz.ts │ │ │ │ ├── service.ts │ │ │ │ └── user-signature.ts │ │ │ ├── serialize/ │ │ │ │ └── index.ts │ │ │ ├── test-utils/ │ │ │ │ ├── index.ts │ │ │ │ └── mock-context.ts │ │ │ ├── transaction/ │ │ │ │ ├── constants.ts │ │ │ │ ├── index.ts │ │ │ │ ├── legacy-polling.js │ │ │ │ ├── legacy-polling.test.js │ │ │ │ ├── transaction-error.test.ts │ │ │ │ ├── transaction-error.ts │ │ │ │ ├── transaction.test.ts │ │ │ │ ├── transaction.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ ├── utils/ │ │ │ │ ├── chain-id/ │ │ │ │ │ ├── chain-id-watcher.test.ts │ │ │ │ │ ├── chain-id-watcher.ts │ │ │ │ │ ├── fetch-chain-id.ts │ │ │ │ │ ├── get-chain-id.test.ts │ │ │ │ │ └── get-chain-id.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── index.ts │ │ │ │ ├── is-react-native.ts │ │ │ │ ├── is.ts │ │ │ │ ├── storage.ts │ │ │ │ └── url.ts │ │ │ ├── wallet-provider-spec/ │ │ │ │ ├── README.md │ │ │ │ ├── assets/ │ │ │ │ │ ├── fcl-ars-auth-v1.excalidraw │ │ │ │ │ ├── fcl-ars-auth-v2.excalidraw │ │ │ │ │ ├── fcl-ars-auth-v3.1.excalidraw │ │ │ │ │ ├── fcl-ars-auth-v3.2.excalidraw │ │ │ │ │ └── fcl-ars-auth-v3.excalidraw │ │ │ │ ├── authorization-function.md │ │ │ │ ├── custodial.md │ │ │ │ ├── draft-v2.md │ │ │ │ ├── draft-v3.md │ │ │ │ ├── draft-v4.md │ │ │ │ ├── draft.md │ │ │ │ ├── non-custodial.md │ │ │ │ ├── provable-authn.md │ │ │ │ ├── user-signature.md │ │ │ │ └── wallet-discover.md │ │ │ └── wallet-utils/ │ │ │ ├── CompositeSignature.ts │ │ │ ├── encode-account-proof.test.ts │ │ │ ├── encode-account-proof.ts │ │ │ ├── index.ts │ │ │ ├── inject-ext-service.ts │ │ │ ├── on-message-from-fcl.ts │ │ │ ├── send-msg-to-fcl.ts │ │ │ └── wallet-utils.test.ts │ │ └── tsconfig.json │ ├── fcl-ethereum-provider/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .eslintrc.json │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __mocks__/ │ │ │ │ └── fcl.ts │ │ │ ├── accounts/ │ │ │ │ ├── account-manager.test.ts │ │ │ │ ├── account-manager.ts │ │ │ │ └── sign-message.test.ts │ │ │ ├── cadence.ts │ │ │ ├── constants.ts │ │ │ ├── create-provider.ts │ │ │ ├── events/ │ │ │ │ ├── event-dispatcher.test.ts │ │ │ │ └── event-dispatcher.ts │ │ │ ├── gateway/ │ │ │ │ ├── gateway.test.ts │ │ │ │ └── gateway.ts │ │ │ ├── hash-utils.test.ts │ │ │ ├── hash-utils.ts │ │ │ ├── index.ts │ │ │ ├── network/ │ │ │ │ ├── network-manager.test.ts │ │ │ │ └── network-manager.ts │ │ │ ├── notifications.ts │ │ │ ├── provider.test.ts │ │ │ ├── provider.ts │ │ │ ├── rpc/ │ │ │ │ ├── handlers/ │ │ │ │ │ ├── eth-accounts.test.ts │ │ │ │ │ ├── eth-accounts.ts │ │ │ │ │ ├── eth-chain-id.test.ts │ │ │ │ │ ├── eth-chain-id.ts │ │ │ │ │ ├── eth-send-transaction.test.ts │ │ │ │ │ ├── eth-send-transaction.ts │ │ │ │ │ ├── eth-signtypeddata.ts │ │ │ │ │ └── personal-sign.ts │ │ │ │ ├── rpc-processor.test.ts │ │ │ │ ├── rpc-processor.ts │ │ │ │ └── types.ts │ │ │ ├── types/ │ │ │ │ ├── account.ts │ │ │ │ ├── eth.ts │ │ │ │ ├── events.ts │ │ │ │ └── provider.ts │ │ │ ├── util/ │ │ │ │ ├── chain.ts │ │ │ │ ├── errors.ts │ │ │ │ ├── eth.ts │ │ │ │ ├── observable.ts │ │ │ │ └── transaction.ts │ │ │ └── wc-provider.ts │ │ └── tsconfig.json │ ├── fcl-rainbowkit-adapter/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .eslintrc.json │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── create-connector.ts │ │ │ ├── fcl-rainbowkit-adapter.test.ts │ │ │ ├── get-wc-connector.ts │ │ │ ├── index.ts │ │ │ ├── use-is-cadence-wallet-connected.ts │ │ │ └── wallets/ │ │ │ ├── flow-wallet.ts │ │ │ └── wc-wallet.ts │ │ └── tsconfig.json │ ├── fcl-react-native/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .eslintrc.json │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── docs/ │ │ │ └── extra.md │ │ ├── docs-generator.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── VERSION.ts │ │ │ ├── client.ts │ │ │ ├── fcl-react-native.ts │ │ │ ├── utils/ │ │ │ │ └── react-native/ │ │ │ │ ├── ConnectModal.js │ │ │ │ ├── ConnectModalProvider.js │ │ │ │ ├── ServiceDiscovery.js │ │ │ │ ├── __tests__/ │ │ │ │ │ └── default-config.test.js │ │ │ │ ├── constants.js │ │ │ │ ├── coreStrategies.js │ │ │ │ ├── default-config.js │ │ │ │ ├── exec-local.js │ │ │ │ ├── index.js │ │ │ │ ├── render-browser.js │ │ │ │ ├── render-deeplink.js │ │ │ │ ├── storage.ts │ │ │ │ └── strategies/ │ │ │ │ ├── deeplink-rpc.js │ │ │ │ ├── discovery-rn.js │ │ │ │ └── utils/ │ │ │ │ ├── browser.js │ │ │ │ └── service-endpoint.js │ │ │ └── walletconnect/ │ │ │ ├── client.ts │ │ │ ├── constants.ts │ │ │ ├── index.ts │ │ │ ├── loader.ts │ │ │ ├── service.ts │ │ │ └── session.ts │ │ └── tsconfig.json │ ├── fcl-wagmi-adapter/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .eslintrc.json │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── fcl-connector.ts │ │ │ ├── fcl-wagmi-adapter.test.ts │ │ │ ├── index.ts │ │ │ └── wc-connector.ts │ │ └── tsconfig.json │ ├── fcl-wc/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .eslintrc.json │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jest.config.js │ │ ├── jest.setup.ts │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── src/ │ │ │ ├── constants.ts │ │ │ ├── fcl-wc.test.ts │ │ │ ├── fcl-wc.ts │ │ │ ├── index.ts │ │ │ ├── mocks/ │ │ │ │ └── file-mock.ts │ │ │ ├── service.ts │ │ │ ├── session.ts │ │ │ ├── store.ts │ │ │ ├── types/ │ │ │ │ ├── declarations.d.ts │ │ │ │ └── types.ts │ │ │ ├── ui/ │ │ │ │ ├── components/ │ │ │ │ │ └── Notification.tsx │ │ │ │ ├── notifications.tsx │ │ │ │ └── styles.css │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ ├── protobuf/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── generated/ │ │ │ │ └── flow/ │ │ │ │ ├── access/ │ │ │ │ │ ├── access_pb.d.ts │ │ │ │ │ ├── access_pb.js │ │ │ │ │ ├── access_pb_service.d.ts │ │ │ │ │ └── access_pb_service.js │ │ │ │ ├── entities/ │ │ │ │ │ ├── account_pb.d.ts │ │ │ │ │ ├── account_pb.js │ │ │ │ │ ├── account_pb_service.d.ts │ │ │ │ │ ├── account_pb_service.js │ │ │ │ │ ├── block_execution_data_pb.d.ts │ │ │ │ │ ├── block_execution_data_pb.js │ │ │ │ │ ├── block_execution_data_pb_service.d.ts │ │ │ │ │ ├── block_execution_data_pb_service.js │ │ │ │ │ ├── block_header_pb.d.ts │ │ │ │ │ ├── block_header_pb.js │ │ │ │ │ ├── block_header_pb_service.d.ts │ │ │ │ │ ├── block_header_pb_service.js │ │ │ │ │ ├── block_pb.d.ts │ │ │ │ │ ├── block_pb.js │ │ │ │ │ ├── block_pb_service.d.ts │ │ │ │ │ ├── block_pb_service.js │ │ │ │ │ ├── block_seal_pb.d.ts │ │ │ │ │ ├── block_seal_pb.js │ │ │ │ │ ├── block_seal_pb_service.d.ts │ │ │ │ │ ├── block_seal_pb_service.js │ │ │ │ │ ├── collection_pb.d.ts │ │ │ │ │ ├── collection_pb.js │ │ │ │ │ ├── collection_pb_service.d.ts │ │ │ │ │ ├── collection_pb_service.js │ │ │ │ │ ├── event_pb.d.ts │ │ │ │ │ ├── event_pb.js │ │ │ │ │ ├── event_pb_service.d.ts │ │ │ │ │ ├── event_pb_service.js │ │ │ │ │ ├── execution_result_pb.d.ts │ │ │ │ │ ├── execution_result_pb.js │ │ │ │ │ ├── execution_result_pb_service.d.ts │ │ │ │ │ ├── execution_result_pb_service.js │ │ │ │ │ ├── metadata_pb.d.ts │ │ │ │ │ ├── metadata_pb.js │ │ │ │ │ ├── metadata_pb_service.d.ts │ │ │ │ │ ├── metadata_pb_service.js │ │ │ │ │ ├── node_version_info_pb.d.ts │ │ │ │ │ ├── node_version_info_pb.js │ │ │ │ │ ├── node_version_info_pb_service.d.ts │ │ │ │ │ ├── node_version_info_pb_service.js │ │ │ │ │ ├── register_pb.d.ts │ │ │ │ │ ├── register_pb.js │ │ │ │ │ ├── register_pb_service.d.ts │ │ │ │ │ ├── register_pb_service.js │ │ │ │ │ ├── transaction_pb.d.ts │ │ │ │ │ ├── transaction_pb.js │ │ │ │ │ ├── transaction_pb_service.d.ts │ │ │ │ │ └── transaction_pb_service.js │ │ │ │ ├── execution/ │ │ │ │ │ ├── execution_pb.d.ts │ │ │ │ │ ├── execution_pb.js │ │ │ │ │ ├── execution_pb_service.d.ts │ │ │ │ │ └── execution_pb_service.js │ │ │ │ └── executiondata/ │ │ │ │ ├── executiondata_pb.d.ts │ │ │ │ ├── executiondata_pb.js │ │ │ │ ├── executiondata_pb_service.d.ts │ │ │ │ └── executiondata_pb_service.js │ │ │ ├── index.js │ │ │ ├── index.test.js │ │ │ └── proto/ │ │ │ └── flow/ │ │ │ ├── access/ │ │ │ │ └── access.proto │ │ │ ├── entities/ │ │ │ │ ├── account.proto │ │ │ │ ├── block.proto │ │ │ │ ├── block_execution_data.proto │ │ │ │ ├── block_header.proto │ │ │ │ ├── block_seal.proto │ │ │ │ ├── collection.proto │ │ │ │ ├── event.proto │ │ │ │ ├── execution_result.proto │ │ │ │ ├── metadata.proto │ │ │ │ ├── node_version_info.proto │ │ │ │ ├── register.proto │ │ │ │ └── transaction.proto │ │ │ ├── execution/ │ │ │ │ └── execution.proto │ │ │ ├── executiondata/ │ │ │ │ └── executiondata.proto │ │ │ └── legacy/ │ │ │ ├── access/ │ │ │ │ └── access.proto │ │ │ ├── entities/ │ │ │ │ ├── account.proto │ │ │ │ ├── block.proto │ │ │ │ ├── block_header.proto │ │ │ │ ├── block_seal.proto │ │ │ │ ├── collection.proto │ │ │ │ ├── event.proto │ │ │ │ └── transaction.proto │ │ │ └── execution/ │ │ │ └── execution.proto │ │ └── webpack.config.js │ ├── react-core/ │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── babel.config.js │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __mocks__/ │ │ │ │ ├── TestProvider.tsx │ │ │ │ ├── flow-client.ts │ │ │ │ ├── tx.ts │ │ │ │ └── user.ts │ │ │ ├── constants.ts │ │ │ ├── core/ │ │ │ │ ├── context.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── hooks/ │ │ │ │ ├── index.ts │ │ │ │ ├── useCrossVmBatchTransaction.test.ts │ │ │ │ ├── useCrossVmBatchTransaction.ts │ │ │ │ ├── useCrossVmBridgeNftFromEvm.test.ts │ │ │ │ ├── useCrossVmBridgeNftFromEvm.ts │ │ │ │ ├── useCrossVmBridgeNftToEvm.test.ts │ │ │ │ ├── useCrossVmBridgeNftToEvm.ts │ │ │ │ ├── useCrossVmBridgeTokenFromEvm.test.ts │ │ │ │ ├── useCrossVmBridgeTokenFromEvm.ts │ │ │ │ ├── useCrossVmBridgeTokenToEvm.test.ts │ │ │ │ ├── useCrossVmBridgeTokenToEvm.ts │ │ │ │ ├── useCrossVmSpendNft.test.ts │ │ │ │ ├── useCrossVmSpendNft.ts │ │ │ │ ├── useCrossVmSpendToken.test.ts │ │ │ │ ├── useCrossVmSpendToken.ts │ │ │ │ ├── useCrossVmTokenBalance.test.ts │ │ │ │ ├── useCrossVmTokenBalance.ts │ │ │ │ ├── useCrossVmTransactionStatus.test.ts │ │ │ │ ├── useCrossVmTransactionStatus.ts │ │ │ │ ├── useFlowAccount.test.ts │ │ │ │ ├── useFlowAccount.ts │ │ │ │ ├── useFlowAuthz.test.ts │ │ │ │ ├── useFlowAuthz.ts │ │ │ │ ├── useFlowBlock.test.ts │ │ │ │ ├── useFlowBlock.ts │ │ │ │ ├── useFlowChainId.test.ts │ │ │ │ ├── useFlowChainId.ts │ │ │ │ ├── useFlowClient.ts │ │ │ │ ├── useFlowConfig.ts │ │ │ │ ├── useFlowCurrentUser.test.ts │ │ │ │ ├── useFlowCurrentUser.ts │ │ │ │ ├── useFlowEvents.test.ts │ │ │ │ ├── useFlowEvents.ts │ │ │ │ ├── useFlowMutate.test.ts │ │ │ │ ├── useFlowMutate.ts │ │ │ │ ├── useFlowNftMetadata.test.ts │ │ │ │ ├── useFlowNftMetadata.ts │ │ │ │ ├── useFlowQuery.test.ts │ │ │ │ ├── useFlowQuery.ts │ │ │ │ ├── useFlowQueryRaw.test.ts │ │ │ │ ├── useFlowQueryRaw.ts │ │ │ │ ├── useFlowRevertibleRandom.test.tsx │ │ │ │ ├── useFlowRevertibleRandom.ts │ │ │ │ ├── useFlowScheduledTransaction.test.ts │ │ │ │ ├── useFlowScheduledTransaction.ts │ │ │ │ ├── useFlowScheduledTransactionCancel.test.ts │ │ │ │ ├── useFlowScheduledTransactionCancel.ts │ │ │ │ ├── useFlowScheduledTransactionList.test.ts │ │ │ │ ├── useFlowScheduledTransactionList.ts │ │ │ │ ├── useFlowScheduledTransactionSetup.test.ts │ │ │ │ ├── useFlowScheduledTransactionSetup.ts │ │ │ │ ├── useFlowTransaction.test.ts │ │ │ │ ├── useFlowTransaction.ts │ │ │ │ ├── useFlowTransactionStatus.test.ts │ │ │ │ └── useFlowTransactionStatus.ts │ │ │ ├── index.ts │ │ │ ├── jest-setup.ts │ │ │ ├── provider/ │ │ │ │ ├── FlowQueryClient.tsx │ │ │ │ ├── GlobalTransactionProvider.tsx │ │ │ │ └── index.ts │ │ │ └── utils/ │ │ │ ├── address.ts │ │ │ ├── deepEqual.ts │ │ │ ├── flowscan.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── react-native-sdk/ │ │ ├── .babelrc │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __mocks__/ │ │ │ │ └── noop.ts │ │ │ ├── components/ │ │ │ │ ├── Connect.tsx │ │ │ │ ├── Profile.tsx │ │ │ │ └── index.ts │ │ │ ├── icons/ │ │ │ │ ├── CheckIcon.tsx │ │ │ │ ├── CopyIcon.tsx │ │ │ │ ├── ExternalLinkIcon.tsx │ │ │ │ ├── LogOutIcon.tsx │ │ │ │ ├── UserIcon.tsx │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── provider/ │ │ │ │ ├── FlowProvider.tsx │ │ │ │ └── index.ts │ │ │ └── styles/ │ │ │ ├── colors.ts │ │ │ ├── dimensions.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── react-sdk/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── src/ │ │ │ ├── __mocks__/ │ │ │ │ ├── fcl.ts │ │ │ │ ├── flow-client.ts │ │ │ │ ├── noop.ts │ │ │ │ ├── tx.ts │ │ │ │ └── user.ts │ │ │ ├── components/ │ │ │ │ ├── Connect.tsx │ │ │ │ ├── NftCard.tsx │ │ │ │ ├── Profile.tsx │ │ │ │ ├── ScheduledTransactionList.tsx │ │ │ │ ├── TransactionButton.tsx │ │ │ │ ├── TransactionDialog.tsx │ │ │ │ ├── TransactionLink.tsx │ │ │ │ ├── index.ts │ │ │ │ └── internal/ │ │ │ │ ├── Button.tsx │ │ │ │ ├── Dialog.tsx │ │ │ │ └── StyleWrapper.tsx │ │ │ ├── core/ │ │ │ │ └── theme.tsx │ │ │ ├── css.d.ts │ │ │ ├── icons/ │ │ │ │ ├── AlertCircleIcon.tsx │ │ │ │ ├── CircleCheckIcon.tsx │ │ │ │ ├── CircleUserRoundIcon.tsx │ │ │ │ ├── CopyIcon.tsx │ │ │ │ ├── DownIcon.tsx │ │ │ │ ├── ExternalLink.tsx │ │ │ │ ├── FlowIcon.tsx │ │ │ │ ├── ImageIcon.tsx │ │ │ │ ├── LoaderCircleIcon.tsx │ │ │ │ ├── LogOutIcon.tsx │ │ │ │ ├── MoreVerticalIcon.tsx │ │ │ │ ├── TrashIcon.tsx │ │ │ │ ├── UserIcon.tsx │ │ │ │ └── XIcon.tsx │ │ │ ├── index.ts │ │ │ ├── jest-setup.ts │ │ │ ├── mocks/ │ │ │ │ └── file-mock.ts │ │ │ ├── provider/ │ │ │ │ ├── DarkModeProvider.tsx │ │ │ │ ├── FlowProvider.tsx │ │ │ │ └── index.ts │ │ │ └── styles/ │ │ │ └── tailwind.css │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ ├── rlp/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .eslintrc.json │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── sdk/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .eslintrc.json │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── TRANSITIONS.md │ │ ├── docs/ │ │ │ └── extra.md │ │ ├── docs-generator.config.js │ │ ├── package.json │ │ ├── readme.md │ │ ├── src/ │ │ │ ├── VERSION.ts │ │ │ ├── account/ │ │ │ │ ├── account.test.ts │ │ │ │ └── account.ts │ │ │ ├── block/ │ │ │ │ ├── block.test.ts │ │ │ │ └── block.ts │ │ │ ├── build/ │ │ │ │ ├── build-arguments.test.ts │ │ │ │ ├── build-arguments.ts │ │ │ │ ├── build-at-block-height.test.ts │ │ │ │ ├── build-at-block-height.ts │ │ │ │ ├── build-at-block-id.test.ts │ │ │ │ ├── build-at-block-id.ts │ │ │ │ ├── build-at-latest-block.test.ts │ │ │ │ ├── build-at-latest-block.ts │ │ │ │ ├── build-authorizations.test.ts │ │ │ │ ├── build-authorizations.ts │ │ │ │ ├── build-get-account.test.ts │ │ │ │ ├── build-get-account.ts │ │ │ │ ├── build-get-block-header.test.ts │ │ │ │ ├── build-get-block-header.ts │ │ │ │ ├── build-get-block.test.ts │ │ │ │ ├── build-get-block.ts │ │ │ │ ├── build-get-collection.test.ts │ │ │ │ ├── build-get-collection.ts │ │ │ │ ├── build-get-events-at-block-height-range.test.ts │ │ │ │ ├── build-get-events-at-block-height-range.ts │ │ │ │ ├── build-get-events-at-block-ids.test.ts │ │ │ │ ├── build-get-events-at-block-ids.ts │ │ │ │ ├── build-get-events.test.ts │ │ │ │ ├── build-get-events.ts │ │ │ │ ├── build-get-latest-block.test.ts │ │ │ │ ├── build-get-latest-block.ts │ │ │ │ ├── build-get-network-parameters.test.ts │ │ │ │ ├── build-get-network-parameters.ts │ │ │ │ ├── build-get-node-version-info.test.ts │ │ │ │ ├── build-get-node-version-info.ts │ │ │ │ ├── build-get-transaction-status.test.ts │ │ │ │ ├── build-get-transaction-status.ts │ │ │ │ ├── build-get-transaction.test.ts │ │ │ │ ├── build-get-transaction.ts │ │ │ │ ├── build-invariant.js │ │ │ │ ├── build-invariant.test.js │ │ │ │ ├── build-limit.test.ts │ │ │ │ ├── build-limit.ts │ │ │ │ ├── build-payer.test.ts │ │ │ │ ├── build-payer.ts │ │ │ │ ├── build-ping.test.ts │ │ │ │ ├── build-ping.ts │ │ │ │ ├── build-proposer.test.ts │ │ │ │ ├── build-proposer.ts │ │ │ │ ├── build-ref.test.ts │ │ │ │ ├── build-ref.ts │ │ │ │ ├── build-script.test.ts │ │ │ │ ├── build-script.ts │ │ │ │ ├── build-subscribe-events.test.ts │ │ │ │ ├── build-subscribe-events.ts │ │ │ │ ├── build-transaction.test.ts │ │ │ │ ├── build-transaction.ts │ │ │ │ ├── build-validator.test.ts │ │ │ │ ├── build-validator.ts │ │ │ │ ├── build-voucher-intercept.test.ts │ │ │ │ ├── build-voucher-intercept.ts │ │ │ │ ├── build.test.ts │ │ │ │ └── build.ts │ │ │ ├── constants.ts │ │ │ ├── context/ │ │ │ │ ├── context.ts │ │ │ │ ├── get-global-transport.test.ts │ │ │ │ ├── get-global-transport.ts │ │ │ │ └── global.ts │ │ │ ├── contract.test.ts │ │ │ ├── decode/ │ │ │ │ ├── README.md │ │ │ │ ├── decode-stream.test.ts │ │ │ │ ├── decode-stream.ts │ │ │ │ ├── decode.test.js │ │ │ │ ├── decode.ts │ │ │ │ └── sdk-decode.ts │ │ │ ├── encode/ │ │ │ │ ├── README.md │ │ │ │ ├── encode.test.ts │ │ │ │ └── encode.ts │ │ │ ├── interaction/ │ │ │ │ ├── interaction.test.ts │ │ │ │ └── interaction.ts │ │ │ ├── node-version-info/ │ │ │ │ └── node-version-info.ts │ │ │ ├── resolve/ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ └── resolve-signatures.test.ts.snap │ │ │ │ ├── __tests__/ │ │ │ │ │ └── resolve-accounts.test.js │ │ │ │ ├── resolve-accounts.test.js │ │ │ │ ├── resolve-accounts.ts │ │ │ │ ├── resolve-arguments.test.ts │ │ │ │ ├── resolve-arguments.ts │ │ │ │ ├── resolve-cadence.test.ts │ │ │ │ ├── resolve-cadence.ts │ │ │ │ ├── resolve-compute-limit.test.ts │ │ │ │ ├── resolve-compute-limit.ts │ │ │ │ ├── resolve-final-normalization.test.ts │ │ │ │ ├── resolve-final-normalization.ts │ │ │ │ ├── resolve-proposer-sequence-number.test.ts │ │ │ │ ├── resolve-proposer-sequence-number.ts │ │ │ │ ├── resolve-ref-block-id.test.ts │ │ │ │ ├── resolve-ref-block-id.ts │ │ │ │ ├── resolve-signatures.test.ts │ │ │ │ ├── resolve-signatures.ts │ │ │ │ ├── resolve-validators.test.ts │ │ │ │ ├── resolve-validators.ts │ │ │ │ ├── resolve-voucher-intercept.test.ts │ │ │ │ ├── resolve-voucher-intercept.ts │ │ │ │ ├── resolve.ts │ │ │ │ └── voucher.ts │ │ │ ├── response/ │ │ │ │ ├── README.md │ │ │ │ ├── __snapshots__/ │ │ │ │ │ └── response.test.ts.snap │ │ │ │ ├── response.test.ts │ │ │ │ └── response.ts │ │ │ ├── sdk-client.ts │ │ │ ├── sdk.test.js │ │ │ ├── sdk.ts │ │ │ ├── test-utils/ │ │ │ │ ├── authz-fn.ts │ │ │ │ ├── index.ts │ │ │ │ ├── mock-send.js │ │ │ │ └── run.ts │ │ │ ├── transport/ │ │ │ │ ├── index.ts │ │ │ │ ├── send/ │ │ │ │ │ ├── send.test.ts │ │ │ │ │ └── send.ts │ │ │ │ └── subscribe/ │ │ │ │ ├── errors.ts │ │ │ │ ├── subscribe-raw.test.ts │ │ │ │ ├── subscribe-raw.ts │ │ │ │ ├── subscribe.test.ts │ │ │ │ ├── subscribe.ts │ │ │ │ └── types.ts │ │ │ └── wallet-utils/ │ │ │ ├── encode-signable.test.ts │ │ │ ├── encode-signable.ts │ │ │ ├── index.ts │ │ │ ├── validate-tx.test.ts │ │ │ └── validate-tx.ts │ │ └── tsconfig.json │ ├── transport-grpc/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── sdk-send-grpc.js │ │ │ ├── send-execute-script.js │ │ │ ├── send-execute-script.test.js │ │ │ ├── send-get-account.js │ │ │ ├── send-get-account.test.js │ │ │ ├── send-get-block-header.js │ │ │ ├── send-get-block-header.test.js │ │ │ ├── send-get-block.js │ │ │ ├── send-get-block.test.js │ │ │ ├── send-get-collection.js │ │ │ ├── send-get-collection.test.js │ │ │ ├── send-get-events.js │ │ │ ├── send-get-events.test.js │ │ │ ├── send-get-network-parameters.js │ │ │ ├── send-get-network-parameters.test.js │ │ │ ├── send-get-node-version-info.js │ │ │ ├── send-get-node-version-info.test.js │ │ │ ├── send-get-transaction-status.js │ │ │ ├── send-get-transaction-status.test.js │ │ │ ├── send-get-transaction.js │ │ │ ├── send-get-transaction.test.js │ │ │ ├── send-grpc.js │ │ │ ├── send-ping.js │ │ │ ├── send-ping.test.js │ │ │ ├── send-transaction.js │ │ │ ├── send-transaction.test.js │ │ │ └── unary.js │ │ └── tsconfig.json │ ├── transport-http/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── send/ │ │ │ │ ├── connect-subscribe-events.test.ts │ │ │ │ ├── connect-subscribe-events.ts │ │ │ │ ├── connect-ws.test.ts │ │ │ │ ├── connect-ws.ts │ │ │ │ ├── http-request.js │ │ │ │ ├── http-request.test.js │ │ │ │ ├── send-execute-script.js │ │ │ │ ├── send-execute-script.test.js │ │ │ │ ├── send-get-account.js │ │ │ │ ├── send-get-account.test.js │ │ │ │ ├── send-get-block-header.js │ │ │ │ ├── send-get-block-header.test.js │ │ │ │ ├── send-get-block.js │ │ │ │ ├── send-get-block.test.js │ │ │ │ ├── send-get-collection.js │ │ │ │ ├── send-get-collection.test.js │ │ │ │ ├── send-get-events.js │ │ │ │ ├── send-get-events.test.js │ │ │ │ ├── send-get-network-parameters.js │ │ │ │ ├── send-get-network-parameters.test.js │ │ │ │ ├── send-get-node-version-info.test.ts │ │ │ │ ├── send-get-node-version-info.ts │ │ │ │ ├── send-get-transaction-status.js │ │ │ │ ├── send-get-transaction-status.test.js │ │ │ │ ├── send-get-transaction.js │ │ │ │ ├── send-get-transaction.test.js │ │ │ │ ├── send-http.ts │ │ │ │ ├── send-ping.test.ts │ │ │ │ ├── send-ping.ts │ │ │ │ ├── send-transaction.js │ │ │ │ ├── send-transaction.test.js │ │ │ │ └── utils.js │ │ │ ├── subscribe/ │ │ │ │ ├── handlers/ │ │ │ │ │ ├── account-statuses.test.ts │ │ │ │ │ ├── account-statuses.ts │ │ │ │ │ ├── block-digests.ts │ │ │ │ │ ├── block-headers.ts │ │ │ │ │ ├── blocks.ts │ │ │ │ │ ├── events.test.ts │ │ │ │ │ ├── events.ts │ │ │ │ │ ├── transaction-statuses.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── models.ts │ │ │ │ ├── subscribe.test.ts │ │ │ │ ├── subscribe.ts │ │ │ │ ├── subscription-manager.test.ts │ │ │ │ ├── subscription-manager.ts │ │ │ │ └── websocket.ts │ │ │ ├── transport.ts │ │ │ └── utils/ │ │ │ ├── combine-urls.test.ts │ │ │ └── combine-urls.ts │ │ └── tsconfig.json │ ├── typedefs/ │ │ ├── .babelrc │ │ ├── .eslintrc.json │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── fvm-errors.ts │ │ │ ├── index.test.js │ │ │ ├── index.ts │ │ │ ├── interaction.ts │ │ │ ├── subscriptions.ts │ │ │ └── transport.ts │ │ └── tsconfig.json │ ├── types/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .eslintrc.json │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── WARNINGS.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── types.test.ts │ │ │ └── types.ts │ │ └── tsconfig.json │ ├── util-actor/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .eslintrc.json │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.test.ts │ │ │ ├── index.ts │ │ │ └── mailbox/ │ │ │ ├── README.md │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── util-address/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .eslintrc.json │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── util-encode-key/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .eslintrc.json │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __snapshots__/ │ │ │ │ └── index.test.ts.snap │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── util-invariant/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .eslintrc.json │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── util-logger/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .eslintrc.json │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── util-logger.test.ts │ │ │ └── util-logger.ts │ │ └── tsconfig.json │ ├── util-rpc/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .eslintrc.json │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── messages.ts │ │ │ ├── rpc-client.test.ts │ │ │ ├── rpc-client.ts │ │ │ └── rpc-error.ts │ │ └── tsconfig.json │ ├── util-semver/ │ │ ├── .babelrc │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── compare-identifiers.js │ │ │ ├── compare.js │ │ │ ├── compare.test.js │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── util-template/ │ │ ├── .babelrc │ │ ├── .browserslistrc │ │ ├── .eslintrc.json │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __snapshots__/ │ │ │ │ └── template.test.ts.snap │ │ │ ├── template.test.ts │ │ │ └── template.ts │ │ └── tsconfig.json │ └── util-uid/ │ ├── .babelrc │ ├── .browserslistrc │ ├── .eslintrc.json │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src/ │ │ ├── util-uid.test.ts │ │ └── util-uid.ts │ └── tsconfig.json └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changeset/README.md ================================================ # Changesets Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@1.7.0/schema.json", "changelog": ["@changesets/changelog-github", {"repo": "onflow/fcl-js"}], "commit": false, "fixed": [], "linked": [], "access": "public", "baseBranch": "master", "updateInternalDependencies": "patch", "ignore": ["@onflow/transport-grpc"] } ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Reporting a Problem/Bug description: Reporting a Problem/Bug title: "[BUG] " labels: [Bug, Needs Triage] body: - type: markdown attributes: value: Please fill out the template below to the best of your ability and include a label indicating which tool/service you were working with when you encountered the problem. - type: textarea attributes: label: Current Behavior description: A concise description of what you're experiencing. validations: required: true - type: textarea attributes: label: Expected Behavior description: A concise description of what you expected to happen. validations: required: true - type: textarea attributes: label: Steps To Reproduce description: Steps to reproduce the behavior. placeholder: | 1. In this environment... 2. With this config... 3. Run '...' 4. See error... validations: required: true - type: textarea attributes: label: Environment description: | examples: - **OS**: Ubuntu 20.04 - **Node**: 13.14.0 - **npm**: 7.6.3 value: | - OS: - Node: - npm: render: markdown validations: required: true - type: textarea attributes: label: What are you currently working on that this is blocking? validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Requesting a Feature or Improvement description: For feature requests. Please search for existing issues first. Also see CONTRIBUTING. title: "[FEATURE] <title>" labels: [Feature, Needs Triage] body: - type: markdown attributes: value: Please fill out the template below to the best of your ability and include a label indicating which tool/service you were working with when you encountered the problem. - type: textarea attributes: label: Issue to be solved description: Please present a concise description of the problem to be addressed by this feature request. Please be clear what parts of the problem are considered to be in-scope and out-of-scope. validations: required: true - type: textarea attributes: label: Suggest A Solution description: | A concise description of your preferred solution. Things to address include: * Details of the technical implementation * Tradeoffs made in design decisions * Caveats and considerations for the future If there are multiple solutions, please present each one separately. Save comparisons for the very end.) validations: required: false - type: textarea attributes: label: What are you currently working on that this is blocking? validations: required: false ================================================ FILE: .github/scripts/prevent-major-bumps.js ================================================ /** * DO NOT REMOVE ME UNLESS YOU KNOW WHAT YOU'RE DOING!! */ const {execSync} = require("child_process") const fs = require("fs") // Fetch the latest changes from the main branch execSync("git fetch origin master") const packageJsons = execSync("git ls-files 'packages/*/package.json'") .toString() .split("\n") .filter(Boolean) // Assert that the package.json files exist if (packageJsons.length === 0) { console.error("Error: No package.json files found.") process.exit(1) // Fail the CI } for (const packageJson of packageJsons) { // Get the current version from package.json const newPackageJson = JSON.parse(fs.readFileSync(packageJson, "utf8")) const newPackageName = newPackageJson.name const newVersion = newPackageJson.version // Get the version from the main branch (or latest release) let prevPackageJson try { prevPackageJson = JSON.parse( execSync(`git show origin/master:${packageJson}`).toString() ) } catch (error) { console.info("Skipping new package", newPackageName) continue } const prevPackageName = prevPackageJson.name const prevVersion = prevPackageJson.version // Assert that the package names match if (newPackageName !== prevPackageName) { console.error( `Error: Package name mismatch for ${newPackageName} (${prevPackageName} -> ${newPackageName}).` ) process.exit(1) // Fail the CI } // Extract major, minor, and patch numbers const newMajor = parseInt(newVersion.split(".")[0]) const prevMajor = parseInt(prevVersion.split(".")[0]) // Check if it's a major version bump if (newMajor > prevMajor) { console.error( `Error: Major version bump detected for ${newPackageName} (${prevVersion} -> ${newVersion}).` ) process.exit(1) // Fail the CI } console.log( `Version bump allowed for ${newPackageName}: ${prevVersion} -> ${newVersion}` ) } process.exit(0) // Allow the CI to pass ================================================ FILE: .github/workflows/add-pitches-to-project.yml ================================================ name: Adds all issues to the project board. on: issues: types: - opened jobs: add-to-project: name: Add issue to project runs-on: ubuntu-latest steps: - uses: actions/add-to-project@v0.4.1 with: project-url: https://github.com/orgs/onflow/projects/85 github-token: ${{ secrets.GH_ACTION_FOR_PROJECTS }} ================================================ FILE: .github/workflows/changeset-check.yml ================================================ # Changeset Enforcement Action # # This workflow ensures that when developers make changes to package code, # they include a "changeset" file that describes what changed and how it # should affect the version number (major/minor/patch). # # Changesets are used by the @changesets/cli tool to automatically: # - Generate changelogs # - Determine version bumps # - Coordinate releases across multiple packages in this monorepo # # The check can be bypassed by adding the "skip-changeset" label to a PR # (useful for docs-only changes, CI fixes, etc. that don't need version bumps) name: Changeset Check on: pull_request: jobs: changeset-check: name: Check for Changeset runs-on: ubuntu-latest # Skip draft PRs - only run on PRs ready for review if: github.event.pull_request.draft == false steps: - name: Checkout uses: actions/checkout@v4 with: # Need full history to compare against base branch fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "18" cache: "npm" - name: Install dependencies run: npm ci # Check if PR has the skip-changeset label to bypass this check - name: Check for opt-out label id: check-labels uses: actions/github-script@v7 with: script: | const labels = context.payload.pull_request.labels.map(label => label.name); const hasOptOut = labels.includes('skip-changeset'); core.setOutput('skip-changeset', hasOptOut); if (hasOptOut) { core.info('Changeset check skipped due to skip-changeset label'); } # Use changesets CLI to check if changeset is needed for this PR - name: Check changeset status id: changeset-check if: steps.check-labels.outputs.skip-changeset != 'true' run: | # First check what packages have changed echo "Checking for package changes..." CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD) PACKAGE_CHANGES=$(echo "$CHANGED_FILES" | grep -E "(packages/.*/src/|packages/.*/package\.json)" || true) if [ -z "$PACKAGE_CHANGES" ]; then echo "status=no-changes" >> $GITHUB_OUTPUT echo "ℹ️ No package changes detected, changeset not required" exit 0 fi echo "📦 Package changes detected:" echo "$PACKAGE_CHANGES" # Now check changeset status - capture both output and exit code echo "Checking changeset status..." set +e # Don't exit on error npx changeset status --since=origin/${{ github.base_ref }} --verbose > changeset-output.txt 2>&1 EXIT_CODE=$? set -e # Re-enable exit on error echo "Changeset status output:" cat changeset-output.txt if [ $EXIT_CODE -eq 0 ]; then echo "status=valid" >> $GITHUB_OUTPUT echo "✅ Changeset requirements satisfied" else echo "status=missing" >> $GITHUB_OUTPUT echo "❌ Missing changeset for package changes" fi # Clean up old changeset comments to avoid PR pollution - name: Clean up old comments uses: actions/github-script@v7 with: script: | // Find and delete old changeset comments from this bot const comments = await github.rest.issues.listComments({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, }); const botComments = comments.data.filter(comment => comment.user.type === 'Bot' && comment.body.includes('📦 Changeset Required') ); for (const comment of botComments) { await github.rest.issues.deleteComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: comment.id, }); console.log(`Deleted old changeset comment: ${comment.id}`); } # Add a helpful comment when changeset is missing - name: Comment on PR - Missing Changeset if: | steps.check-labels.outputs.skip-changeset != 'true' && steps.changeset-check.outputs.status == 'missing' uses: actions/github-script@v7 with: script: | const comment = `## 📦 Changeset Required This PR appears to modify package code but doesn't include a changeset. A changeset helps track version changes and generate release notes. ### To add a changeset: \`\`\`bash npm run changeset \`\`\` ### To skip this check (if no version bump is needed): Add the \`skip-changeset\` label to this PR`; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: comment }); # Determine final status and fail the check if needed - name: Set final status id: final-status run: | if [ "${{ steps.check-labels.outputs.skip-changeset }}" = "true" ]; then echo "status=skipped" >> $GITHUB_OUTPUT echo "✅ Changeset check skipped (skip-changeset label detected)" elif [ "${{ steps.changeset-check.outputs.status }}" = "no-changes" ]; then echo "status=not-required" >> $GITHUB_OUTPUT echo "✅ Changeset not required (no package changes)" elif [ "${{ steps.changeset-check.outputs.status }}" = "valid" ]; then echo "status=success" >> $GITHUB_OUTPUT echo "✅ Changeset requirements satisfied" else echo "status=missing" >> $GITHUB_OUTPUT echo "❌ Missing required changeset" exit 1 fi # Create a summary for the GitHub Actions UI - name: Summary run: | case "${{ steps.final-status.outputs.status }}" in "skipped") echo "### ✅ Changeset Check: Skipped" >> $GITHUB_STEP_SUMMARY echo "The changeset requirement was bypassed using the skip-changeset label." >> $GITHUB_STEP_SUMMARY ;; "not-required") echo "### ✅ Changeset Check: Not Required" >> $GITHUB_STEP_SUMMARY echo "No package changes detected, so no changeset is needed." >> $GITHUB_STEP_SUMMARY ;; "success") echo "### ✅ Changeset Check: Passed" >> $GITHUB_STEP_SUMMARY echo "Changeset requirements satisfied for this PR." >> $GITHUB_STEP_SUMMARY ;; "missing") echo "### ❌ Changeset Check: Failed" >> $GITHUB_STEP_SUMMARY echo "Package changes detected but no valid changeset found." >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**To fix:** Run \`npm run changeset\` or add the \`skip-changeset\` label if no version bump is needed." >> $GITHUB_STEP_SUMMARY ;; esac ================================================ FILE: .github/workflows/code-analysis.yml ================================================ # This workflow is used to run CodeQL analysis on the codebase. # If a code scanning alert is found, it will fail the PR, # and prompt the user to fix the issue in a comment. name: "CodeQL Analysis" on: push: branches: - master pull_request: branches: - master schedule: - cron: "0 0 * * *" jobs: analyze-code: runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write pull-requests: write strategy: matrix: languages: [javascript-typescript] fail-fast: false steps: - name: Checkout repository uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.languages }} queries: security-extended - name: Autobuild uses: github/codeql-action/autobuild@v3 - name: Analyze uses: github/codeql-action/analyze@v3 with: category: "/language:${{ matrix.languages}}" ================================================ FILE: .github/workflows/dependancy-review.yml ================================================ # Dependency Review Action # PRs introducing NEW known-vulnerable packages will be blocked from merging. # This will output a GHAS comment in the PR with the details of the vulnerabilities. # and will also provide a comment on what to do next. # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement name: "Dependency review" on: pull_request: branches: ["master"] permissions: contents: read pull-requests: write # Required for PR comments jobs: dependency-review: runs-on: ubuntu-latest outputs: vulnerable-changes: ${{ steps.review.outputs.vulnerable-changes }} steps: - name: "Checkout repository" uses: actions/checkout@v4 - name: "Dependency Review" id: review uses: actions/dependency-review-action@v4 with: comment-summary-in-pr: always fail-on-severity: moderate #allow-ghsas: GHSA-q34m-jh98-gwm2,GHSA-f9vj-2wh5-fj8j EXAMPLE of how to whitelist! dependency-review-failure-info: needs: dependency-review if: failure() runs-on: ubuntu-latest steps: - name: Add PR Comment uses: actions/github-script@v7 env: VULN_OUTPUT: ${{ needs.dependency-review.outputs.vulnerable-changes }} with: script: | try { const vulnData = JSON.parse(process.env.VULN_OUTPUT || '[]'); let details = ''; for (const pkg of vulnData) { details += `\n📦 **${pkg.name}@${pkg.version}**\n`; } const comment = `⚠️ **Security Dependency Review Failed** ⚠️ This pull request introduces dependencies with security vulnerabilities of moderate severity or higher. ### Vulnerable Dependencies:${details} ### What to do next? 1. Review the vulnerability details in the Dependency Review Comment above, specifically the "Vulnerabilities" section 2. Click on the links in the "Vulnerability" section to see the details of the vulnerability 3. If multiple versions of the same package are vulnerable, please update to the common latest non-vulnerable version 4. If you are unsure about the vulnerability, please contact the security engineer 5. If the vulnerability cannot be avoided (can't upgrade, or need to keep), contact #security on slack to **get it added to the allowlist** \nSecurity Engineering contact: #security on slack`; await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: comment }); } catch (error) { console.error('Error processing vulnerability data:', error); throw error; } ================================================ FILE: .github/workflows/integrate.yml ================================================ name: FLOW-JS-SDK Continuous Integration on: pull_request: jobs: test_pull_request: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v1 with: node-version: 18.x # This step is VERY important. Changesets has a bug where peer dependencies will cause major version bumps # This script will prevent that from happening. You must manually edit any changesets PRs which have this issue # to correct the version bump. # # If you wish to allow a major version bump, you will have to override this check, but be VERY careful with it. # # See: # https://github.com/changesets/changesets/pull/1132 # https://github.com/changesets/changesets/issues/1011 # https://github.com/changesets/changesets/issues/960 # https://github.com/changesets/changesets/issues/822 #- name: Prevent Major Version Bumps # run: node ./.github/scripts/prevent-major-bumps.js - run: make ci ================================================ FILE: .github/workflows/promote-playground.yml ================================================ name: Promote playground on release on: # Automatically trigger after Release workflow completes on master workflow_run: workflows: ["Release"] types: - completed branches: - master # Optional: Allow manual triggering (will auto-detect latest tag) workflow_dispatch: branches: - master permissions: contents: write concurrency: group: promote-playground cancel-in-progress: false jobs: promote: runs-on: ubuntu-latest # Only run if Release workflow succeeded (or if manually triggered) # Note: workflow_run already filters for master branch, so we only need to check success if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} steps: - name: Checkout with full history uses: actions/checkout@v4 with: fetch-depth: 0 - name: Move playground branch to tag run: | echo "Setting up git configuration..." git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" echo "Fetching tags..." git fetch --tags # Check if this is a manual trigger (skip time check for manual runs) if [ "${{ github.event_name }}" != "workflow_dispatch" ]; then echo "Checking if a new tag was created recently..." # Get the latest stable tag first (excluding alpha/beta/rc) LATEST_STABLE_TAG=$(git tag -l "@onflow/react-sdk@*" --sort=-version:refname | grep -v -E '(alpha|beta|rc)' | head -n 1) # Get the timestamp of that specific stable tag LATEST_TAG_TIME=$(git log -1 --format='%ct' "$LATEST_STABLE_TAG") CURRENT_TIME=$(date +%s) TIME_DIFF=$((CURRENT_TIME - LATEST_TAG_TIME)) if [ $TIME_DIFF -gt 3600 ]; then echo "No new @onflow/react-sdk tag created within the last hour" exit 0 fi echo "New tag detected within the last hour, proceeding with promotion" else echo "Manual trigger detected, skipping time check" fi # Get the latest stable @onflow/react-sdk tag (excluding alpha/beta/rc versions) TAG=$(git tag -l "@onflow/react-sdk@*" --sort=-version:refname | grep -v -E '(alpha|beta|rc)' | head -n 1) if [ -z "$TAG" ]; then echo "Error: No @onflow/react-sdk tag found" exit 1 fi echo "Found latest tag: $TAG" git show --no-patch --pretty=fuller "$TAG" # Create/update branch at the tag commit and push echo "Updating playground branch to point to $TAG..." git branch -f playground "$TAG" git push --force-with-lease origin playground echo "Successfully updated playground branch to $TAG" ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: branches: - master pull_request: types: [closed] branches: - master concurrency: ${{ github.workflow }}-${{ github.ref }} jobs: # Job 1: Create/update Version PR (on push to master) version: if: github.event_name == 'push' name: Create Version PR runs-on: ubuntu-latest permissions: contents: write pull-requests: write steps: - name: Checkout Repo uses: actions/checkout@v4 with: # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 - name: Install Dependencies run: npm i - name: Create Release Pull Request uses: changesets/action@v1 with: title: "Version Packages" commit: "Version Packages" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Job 2: Publish to npm (when Version PR is merged) publish: if: github.event_name == 'pull_request' && github.event.pull_request.merged == true && startsWith(github.event.pull_request.title, 'Version Packages') name: Publish to npm runs-on: ubuntu-latest permissions: id-token: write contents: write steps: - name: Checkout Repo uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 registry-url: "https://registry.npmjs.org" - name: Upgrade npm for OIDC support run: npm install -g npm@latest - name: Install Dependencies run: npm i - name: Build packages run: npm run build - name: Publish to npm uses: changesets/action@v1 with: publish: npx changeset publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Clear tokens to allow OIDC authentication # https://github.com/orgs/community/discussions/176761 # https://github.com/changesets/changesets/issues/1152 NODE_AUTH_TOKEN: "" NPM_TOKEN: "" ================================================ FILE: .gitignore ================================================ # folders node_modules/ dist/ .parcel-cache/ flowdb/ .exrc .DS_Store # file types dev-wallet/db/*.db todo.md **/debug.log *.log .vscode/* # type declarations packages/*/types/ # generated documentation /docs-generator/output/* ================================================ FILE: .prettierignore ================================================ README.md *.md *.mdx packages/protobuf/src/generated/ **/*.hbs ================================================ FILE: .prettierrc ================================================ { "semi": false, "trailingComma": "es5", "bracketSpacing": false, "arrowParens": "avoid", "plugins": ["prettier-plugin-tailwindcss", "prettier-plugin-classnames"] } ================================================ FILE: AGENTS.md ================================================ # AGENTS.md Guidance for AI coding agents (Claude Code, Codex, Cursor, Copilot, and others) working in this repository. Loaded into agent context automatically — keep edits concise. ## Overview `fcl-js` is the TypeScript/JavaScript **Flow Client Library** monorepo: the reference client for connecting browser and server apps to the Flow blockchain and FCL-compatible wallets (`README.md`). It is a **Lerna monorepo with independent package versioning** (`lerna.json`: `"version": "independent"`) using **npm workspaces** (`package.json`: `"workspaces": ["./packages/*"]`), with releases driven by **Changesets** from the `master` branch (`.changeset/config.json`, `.github/workflows/release.yml`). The flagship package is `@onflow/fcl` (`packages/fcl/`); `@onflow/sdk` (`packages/sdk/`) is the lower-level Flow Access API client FCL is built on. ## Build and Test Commands This repo uses **npm + Lerna**, not pnpm or yarn. There is no `pnpm-workspace.yaml`; the committed lockfile is `package-lock.json`. Root scripts (`package.json`): - `npm i` — install workspace dependencies (CONTRIBUTING.md). `make install` runs `npm ci`. - `npm run build` — `lerna run build` across all packages. - `npm test` — runs `jest` with `projects: ["<rootDir>/packages/*"]` (`jest.config.js`). - `npm start` — `npm run build && lerna run start --parallel` (per-package watch mode). - `npm run prettier:check` / `npm run prettier` — check/write formatting. - `npm run generate-all-docs` — `node docs-generator/generate-all-docs.js`; scans packages that declare a `generate-docs` script and invokes each one. - `npm run demo` — runs emulator + dev-wallet + Vite in `packages/demo` (requires `flow` CLI). - `npm run demo:testnet` — same demo app pointed at testnet. - `npm run changeset` / `npm run release` — changeset CLI / `build && changeset publish`. Makefile wrappers (`Makefile`): - `make all` → `clean install build test`. - `make ci` → `clean install build`, then `npm run test -- --ci` and `npm run prettier:check`. This is exactly what `.github/workflows/integrate.yml` runs on every PR. - `make clean` — deletes `node_modules/`, `dist/`, `types/` inside every `packages/*`. - `make publish` — runs `npm publish` in every package (not typical; CI uses Changesets). Per-package scripts are uniform (see `packages/fcl/package.json`, `packages/sdk/package.json`, etc.): `test` → `jest`, `build` → `fcl-bundle` (often preceded by `eslint .`), `start` → `fcl-bundle --watch`, `build:types` → `tsc` (where present). The bundler is the in-tree `@onflow/fcl-bundle` package (`packages/fcl-bundle/`, wraps Rollup). To work on one package, `cd packages/<name> && npm test` (or `npm run test:watch` where declared). CI uses **Node 18** for PRs (`integrate.yml`) and **Node 22** for releases (`release.yml`). No `engines` field is declared in any package.json. ## Architecture Everything ships from `packages/*`. Verified packages (29): **Flagship / entry points** - `fcl/` — `@onflow/fcl`, the browser-first high-level client (wallet discovery, authn, `query`, `mutate`, transactions, signatures). - `fcl-core/` — `@onflow/fcl-core`, platform-agnostic core that `fcl` and `fcl-react-native` build on. Houses `wallet-provider-spec/` and `wallet-utils/`. - `fcl-react-native/` — `@onflow/fcl-react-native`, React Native variant of FCL. - `sdk/` — `@onflow/sdk`, low-level Flow Access API client (builders, interactions, encode/ decode, resolvers). FCL wraps this. **React kits** - `react-core/` — `@onflow/react-core`, platform-agnostic React hooks (TanStack Query-based). - `react-sdk/` — `@onflow/react-sdk`, web React bindings built on `react-core` + `fcl`. - `react-native-sdk/` — `@onflow/react-native-sdk`, RN bindings built on `react-core` + `fcl-react-native`. **EVM / cross-VM** - `fcl-ethereum-provider/` — EIP-1193 Ethereum provider backed by an FCL wallet. - `fcl-wagmi-adapter/` — Wagmi connector built on `fcl-ethereum-provider`. - `fcl-rainbowkit-adapter/` — RainbowKit adapter built on `fcl-wagmi-adapter`. **Wallet connectivity** - `fcl-wc/` — `@onflow/fcl-wc`, WalletConnect v2 adapter for FCL. **Transports / encoding / types** - `transport-http/` — HTTP transport against the Flow Access API (used by `sdk`). - `transport-grpc/` — gRPC-Web transport. **Ignored by Changesets** (`.changeset/config.json` `"ignore": ["@onflow/transport-grpc"]`) — do not add changesets for it. - `protobuf/` — `@onflow/protobuf`, generated gRPC protobuf bindings (built via webpack, not `fcl-bundle`). - `rlp/` — RLP encoder port (MPL-2.0; every other package is Apache-2.0). - `types/` — Cadence value type codecs (`t.Int`, `t.Address`, …). - `typedefs/` — `@onflow/typedefs`, public TypeScript definitions (see README § TypeScript). **Shared utilities** (`util-*`) - `util-actor`, `util-address`, `util-encode-key`, `util-invariant`, `util-logger`, `util-rpc`, `util-semver`, `util-template`, `util-uid`. **Config + tooling + dev** - `config/` — `@onflow/config`, FCL's config store (e.g. `fcl.config({...})`). - `fcl-bundle/` — the internal Rollup-based bundler every publishable package uses. - `demo/` — `@onflow/demo` (private), Vite app for manual testing against emulator/testnet/ mainnet. See `packages/demo/README.md`. ## Conventions and Gotchas - **Do not switch package managers.** The repo is npm + Lerna + Changesets. Switching to pnpm/yarn breaks lockfile and CI. - **Do not add a top-level integration test.** `jest.config.js` aggregates `packages/*` Jest projects; tests live inside each package next to source (`*.test.ts`). - **Write a changeset for any package-facing change.** Run `npx changeset` per CONTRIBUTING.md. The baseBranch is `master`, not `main`. - **`@onflow/transport-grpc` is Changeset-ignored** — do not create a changeset entry targeting it (`.changeset/config.json`). - **Never edit `packages/protobuf/src/generated/`** — regenerate via the package's `generate` script (`protoc ...`). This path is also in `.prettierignore`. - **Versions are independent.** Lerna `"version": "independent"`; bumping `@onflow/fcl` does not bump siblings. Cross-package imports use pinned versions — update them explicitly when changing a shared util's API. - **Builds run lint.** Most packages define `"build": "npm run lint && fcl-bundle"`; a lint error will fail `npm run build` and therefore `make ci`. - **`@onflow/fcl-bundle` is in-tree.** When debugging build output, edit `packages/fcl-bundle/` rather than searching for it in `node_modules`. - **Commit message format** (CONTRIBUTING.md): `TOPIC -- [package-name] description`. Topics: `PKG` (package change, needs changelog), `DOC`, `OPS`, `VSN`. - **Prettier config**: `semi: false`, `bracketSpacing: false`, `arrowParens: "avoid"` (`.prettierrc`). CI runs `prettier --check .` as part of `make ci`. - **Wallet provider spec** lives in the repo at `packages/fcl-core/src/wallet-provider-spec/` (active draft: `draft-v4.md`). Link here rather than duplicating spec text. - **`@onflow/sdk` vs `@onflow/fcl`**: SDK is low-level (Access API only, no wallet). FCL depends on SDK. Don't reimplement SDK primitives inside `fcl` or `fcl-core`. ## Files Not to Modify - `packages/protobuf/src/generated/` — generated from `.proto` (also in `.prettierignore`). - `package-lock.json` — regenerate via `npm i`, don't hand-edit. - `packages/*/CHANGELOG.md` — managed by Changesets. - `packages/*/dist/`, `packages/*/types/` — build output (cleaned by `make clean`). ================================================ FILE: CHANGELOG.md ================================================ # Changelog Release notes and version history for fcl-js are tracked via GitHub Releases: - https://github.com/onflow/fcl-js/releases For user-facing changes per version, see the Releases page. ================================================ FILE: CITATION.cff ================================================ cff-version: 1.2.0 message: "If you use fcl-js in your research or reference it, please cite it as below." title: "fcl-js: Flow Client Library for JavaScript and TypeScript" authors: - name: "Flow Foundation" website: "https://flow.com" repository-code: "https://github.com/onflow/fcl-js" url: "https://flow.com" license: Apache-2.0 type: software keywords: - flow - flow-network - fcl - flow-client-library - typescript - javascript - sdk - wallet-authentication - dapp - cadence ================================================ FILE: CODEOWNERS ================================================ * @onflow/fcl ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at <community@onflow.org>. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to FCL and the Flow JS SDK The following is a set of guidelines for contributing to FCL and the Flow JS SDK These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. ## Table Of Contents [Getting Started](#project-overview) [How Can I Contribute?](#how-can-i-contribute) - [Reporting Bugs](#reporting-bugs) - [Suggesting Enhancements](#suggesting-enhancements) - [Your First Code Contribution](#your-first-code-contribution) - [Pull Requests](#pull-requests) - [Building the Repo](#building-the-repo) - [Release Process](#release-process) [Styleguides](#styleguides) - [Git Commit Messages](#git-commit-messages) - [JavaScript Styleguide](#javascript-styleguide) [Additional Notes](#additional-notes) ## How Can I Contribute? ### Reporting Bugs #### Before Submitting A Bug Report - **Search existing issues** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one. #### How Do I Submit A (Good) Bug Report? Explain the problem and include additional details to help maintainers reproduce the problem: - **Use a clear and descriptive title** for the issue to identify the problem. - **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, **don't just say what you did, but explain how you did it**. - **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). - **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. - **Explain which behavior you expected to see instead and why.** - **Include error messages and stack traces** which show the output / crash and clearly demonstrate the problem. Provide more context by answering these questions: - **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens. Include details about your configuration and environment: - **What is the version of the Cadence you're using**? - **What's the name and version of the Operating System you're using**? ### Suggesting Enhancements #### Before Submitting An Enhancement Suggestion - **Perform a cursory search** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. #### How Do I Submit A (Good) Enhancement Suggestion? Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). Create an issue and provide the following information: - **Use a clear and descriptive title** for the issue to identify the suggestion. - **Provide a step-by-step description of the suggested enhancement** in as many details as possible. - **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. - **Explain why this enhancement would be useful** to Cadence users. ### Your First Code Contribution Unsure where to begin contributing to Cadence? You can start by looking through these `good-first-issue` and `help-wanted` issues: - [Good first issues][https://github.com/onflow/cadence/labels/good%20first%20issue]: issues which should only require a few lines of code, and a test or two. - [Help wanted issues][https://github.com/onflow/cadence/labels/help%20wanted]: issues which should be a bit more involved than `good-first-issue` issues. Both issue lists are sorted by total number of comments. While not perfect, number of comments is a reasonable proxy for impact a given change will have. ### Pull Requests The process described here has several goals: - Maintain code quality - Fix problems that are important to users - Engage the community in working toward the best possible Developer/User Experience - Enable a sustainable system for the Cadence's maintainers to review contributions Please follow the [styleguides](#styleguides) to have your contribution considered by the maintainers. Reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. ### Building the Repo ```shell npm i npm run build ``` ### Release Process Packages stable versions releases are controlled by [changesets](https://github.com/changesets/changesets) from the `master` branch #### Creating a changeset Changesets are used to track changes to the packages in the repository. They are used to generate changelogs and determine the next version of the package. To create a changeset, run: ``` npx changeset ``` #### Prerelease(alpha) In order to create an `alpha` (pre-)release - create a branch with `release-<VERSION>` as a branch name - run: ``` npm run changeset pre enter alpha npm run changeset version npm run changeset publish ``` *NOTE: you need to have an npm account and be a member of [OnFlow organization](https://www.npmjs.com/org/onflow)* `changeset` commands should preferably be run from the `release` branch and not from feature branches in order to avoid merge conflicts with other feature branches when the release is ready to be published as stable run from the release branch ``` npm run changeset pre exit ``` and merge `release-<VERSION>` branch to `master` ## Styleguides ### Git Commit Messages - Use the present tense ("Add feature" not "Added feature") - Use the imperative mood ("Move cursor to..." not "Moves cursor to...") - Limit the first line to 72 characters or less - Reference issues and pull requests liberally after the first line - Start commits with a 3 character topic - Topics: - PKG -- Modified a package (Requires a Changelog Entry) - DOC -- Modified a some form of documentation - OPS -- Modified repository tooling/scripts - VSN -- Changed a version number - Scope commits to individual packages and include package name in commit message Format: TOPIC -- [package-name] what is change Examples: - OPS -- [root] tests only run once now for ci master branch - PKG -- [decode] fix issue with using regex for type matching - DOC -- [root] update readme with emulator instructions - VSN -- [interaction] 0.0.3 -> 0.0.4 - VSN -- [fcl] 1.5.3 -> 2.0.0 breaking change - VSN -- [sdk] @onflow/send 0.0.5 -> 0.0.6 ### JavaScript Styleguide Use Prettier with the setting in the root `.prettierrc` file ## Additional Notes Thank you for your interest in contributing to the FCL and Flow JS SDK ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2019-2021 Dapper Labs, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ PACKAGES := ls -d1 packages/* RUN := xargs -n1 -t EXEC := $(PACKAGES) | $(RUN) .PHONY: default default: $(info Available Make Commands) @awk -F: '/^[A-z\-_]+\:/ {print " - make " $$1}' Makefile | sort .PHONY: all all: clean install build test $(info TSK all) .PHONY: doctor doctor: clean install build test $(info TSK: doctor) .PHONY: clean clean: $(info TSK clean) $(info delete packages/*/node_modules) $(EXEC) sh -c 'rm -rf $$0/node_modules' $(info delete packages/*/dist) $(EXEC) sh -c 'rm -rf $$0/dist' $(info delete packages/*/types) $(EXEC) sh -c 'rm -rf $$0/types' .PHONY: install install: $(info TSK: install) sh -c 'npm ci || exit 255' .PHONY: build build: $(info TSK: build) sh -c 'npm run build || exit 255' .PHONY: test test: $(info TSK: test) sh -c 'npm run test || exit 255' .PHONY: publish publish: $(info TSK: publish) $(info run "npm publish" in packages/*) $(EXEC) sh -c 'npm publish $$0' .PHONY: ci ci: clean install build $(info TSK: ci) sh -c 'npm run test -- --ci || exit 255' sh -c 'npm run prettier:check || exit 255' ================================================ FILE: NOTICE ================================================ Flow Copyright 2019-2020 Dapper Labs, Inc. This product includes software developed at Dapper Labs, Inc. (https://www.dapperlabs.com/). ================================================ FILE: README.md ================================================ # fcl-js — Flow Client Library for JavaScript and TypeScript [![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](./LICENSE) [![Release](https://img.shields.io/github/v/release/onflow/fcl-js?include_prereleases&sort=semver)](https://github.com/onflow/fcl-js/releases) [![Discord](https://img.shields.io/discord/613813861610684416?label=Discord&logo=discord&logoColor=white)](https://discord.gg/flow) [![Built on Flow](https://img.shields.io/badge/Built%20on-Flow-00EF8B?logo=flow)](https://flow.com) [![npm (@onflow/fcl)](https://img.shields.io/npm/v/@onflow/fcl.svg?label=%40onflow%2Ffcl)](https://www.npmjs.com/package/@onflow/fcl) [![FLOW-JS-SDK Continuous Integration](https://github.com/onflow/flow-js-sdk/actions/workflows/integrate.yml/badge.svg)](https://github.com/onflow/flow-js-sdk/actions/workflows/integrate.yml) [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lerna.js.org/) <p align="center"> <i>Connect your dapp to users, their wallets and Flow.</i> <br /> <br /> <a href="https://developers.flow.com/tutorials/flow-app-quickstart">Quickstart</a> · <a href="https://github.com/onflow/fcl-js/issues">Report Bug</a> · <a href="https://github.com/onflow/fcl-js/blob/master/CONTRIBUTING.md">Contribute</a> </p> ## TL;DR - **What:** FCL (Flow Client Library) in TypeScript and JavaScript. Connect Flow dApps to wallets, send transactions, and read on-chain data from the browser. - **Who it's for:** JavaScript and TypeScript developers building dApps, wallets, and tools on Flow. - **Why use it:** Standardized wallet discovery and authentication, Cadence script and transaction execution, and a flexible runtime that works in browser and server environments. - **Status:** see [Releases](https://github.com/onflow/fcl-js/releases) for the latest version. - **License:** Apache-2.0. - **Related repos:** [onflow/flow-go-sdk](https://github.com/onflow/flow-go-sdk) · [onflow/fcl-discovery](https://github.com/onflow/fcl-discovery) · [onflow/fcl-dev-wallet](https://github.com/onflow/fcl-dev-wallet) - The reference JavaScript/TypeScript client library for the Flow network, open-sourced since 2020. ## 🌟 What is FCL? The **Flow Client Library (FCL) JS** is a package designed to facilitate interactions between dapps, wallets, and the Flow network. It provides a standardized way for applications to connect with users and their wallets, **eliminating the need for custom integrations**. ### 🔑 Key Features: - 🔌 **Universal Wallet Support** – Works seamlessly with all FCL-compatible wallets, making authentication simple. - 🔐 **Secure Authentication** – Standardized authentication flow ensures a smooth user experience. - ⚡ **Blockchain Interactions** – Enables querying, mutating, and interacting with smart contracts on Flow. - 🛠️ **Full-Featured Utilities** – Offers built-in functions to streamline blockchain development. - 🌍 **Flexible Environment** – Can run in both browser and server environments, though wallet interactions are browser-only. FCL was created to make building Flow-connected applications **easy, secure, and scalable** by defining **standardized communication patterns** between wallets, applications, and users. For iOS, we also offer [FCL Swift](https://github.com/Outblock/fcl-swift). --- ## Getting Started ### Requirements - Node version `v12.0.0 or higher`. ### Installation To use the FCL JS in your application, install using **yarn** or **npm** ```shell npm i -S @onflow/fcl ``` ```shell yarn add @onflow/fcl ``` #### Importing **ES6** ```js import * as fcl from "@onflow/fcl"; ``` **Node.js** ```js const fcl = require("@onflow/fcl"); ``` --- ## FCL for Dapps #### Wallet Interactions - *Wallet Discovery* and *Sign-up/Login*: Onboard users with ease. Never worry about supporting multiple wallets. Authenticate users with any [FCL compatible wallet](#current-wallet-providers). ```js // in the browser import * as fcl from "@onflow/fcl" fcl.config({ "discovery.wallet": "https://fcl-discovery.onflow.org/testnet/authn", // Endpoint set to Testnet }) fcl.authenticate() ``` ![FCL Default Discovery UI](docs/images/discovery.png) > **Note**: A [Dapper Wallet](https://meetdapper.com/developers) developer account is required. To enable Dapper Wallet inside FCL, you need to [follow this guide](https://docs.meetdapper.com/get-started). - *Interact with smart contracts*: Authorize transactions via the user's chosen wallet - *Prove ownership of a wallet address*: Signing and verifying user signed data [Learn more about wallet interactions >](https://developers.flow.com/tooling/fcl-js/api#wallet-interactions) #### Blockchain Interactions - *Query the chain*: Send arbitrary Cadence scripts to the chain and receive back decoded values ```js import * as fcl from "@onflow/fcl"; const result = await fcl.query({ cadence: ` access(all) fun main(a: Int, b: Int, addr: Address): Int { log(addr) return a + b } `, args: (arg, t) => [ arg(7, t.Int), // a: Int arg(6, t.Int), // b: Int arg("0xba1132bc08f82fe2", t.Address), // addr: Address ], }); console.log(result); // 13 ``` - *Mutate the chain*: Send arbitrary transactions with your own signatures or via a user's wallet to perform state changes on chain. > **Note:** The Cadence snippet below uses pre-1.0 syntax (`AuthAccount`, `account.borrow`, private paths). Cadence 1.0 replaced these with `auth(...) &Account`, `account.storage.borrow`, and storage-only paths. This example has not yet been updated — refer to the [Cadence migration guide](https://cadence-lang.org/docs/cadence-migration-guide) when writing new transactions. ```js import * as fcl from "@onflow/fcl"; // in the browser, FCL will automatically connect to the user's wallet to request signatures to run the transaction const txId = await fcl.mutate({ cadence: ` import Profile from 0xba1132bc08f82fe2 transaction(name: String) { prepare(account: AuthAccount) { account.borrow<&{Profile.Owner}>(from: Profile.privatePath)!.setName(name) } } `, args: (arg, t) => [arg("myName", t.String)], }); ``` [Learn more about on-chain interactions >](https://developers.flow.com/tooling/fcl-js/api#on-chain-interactions) #### Utilities - Get account details from any Flow address - Get the latest block - Transaction status polling - Event polling - Custom authorization functions [Learn more about utilities >](https://developers.flow.com/tooling/fcl-js/api#pre-built-interactions) ## Typescript Support FCL JS supports TypeScript. If you need to import specific types, you can do so via the [@onflow/typedefs](./packages/typedefs/README.md) package. ```typescript import {CurrentUser} from "@onflow/typedefs" const newUser: CurrentUser = { addr: null, cid: null, expiresAt: null, f_type: 'User', f_vsn: '1.0.0', loggedIn: null, services: [] } ``` For all type definitions available, see [this file](./packages/typedefs/src/index.ts) ## Next Steps - See the [Flow App Quick Start](https://developers.flow.com/build/getting-started/fcl-quickstart). - See the full [API Reference](https://developers.flow.com/tools/clients/fcl-js/api) for all FCL functionality. - Learn Cadence — the smart contract language for Flow — to build scripts and transactions: [Cadence](https://cadence-lang.org). - Explore all of Flow [docs and tools](https://developers.flow.com). ## Development & Testing ### Internal Demo App A lightweight demo application is available for testing FCL and Kit packages during development: ```bash # Build all packages npm run build # Start the demo npm run demo ``` The demo provides: - **Hot Reload**: Instant feedback for code changes - **Focused Testing**: Clean interface for testing authentication, queries, and transactions - **Debug Tools**: Real-time inspection of FCL state and responses - **Lightweight**: Vite-based setup for fast iteration - **Workspace Integration**: Proper Lerna workspace setup with automatic symlinks See [`packages/demo/README.md`](./packages/demo/README.md) for detailed usage instructions. --- ## FCL for Wallet Providers Wallet providers on Flow have the flexibility to build their user interactions and UI through a variety of ways: - Front channel communication via Iframe, pop-up, tab, or extension - Back channel communication via HTTP FCL is agnostic to the communication channel and be configured to create both custodial and non-custodial wallets. This enables users to interact with wallet providers without needing to download an app or extension. The communication channels involve responding to a set of pre-defined FCL messages to deliver the requested information to the dapp. Implementing a FCL compatible wallet on Flow is as simple as filling in the responses with the appropriate data when FCL requests them. If using any of the front-channel communication methods, FCL also provides a set of [wallet utilities](https://github.com/onflow/fcl-js/blob/master/packages/fcl-core/src/wallet-utils/index.js) to simplify this process. ### Current Wallet Providers - [Flow Wallet](https://wallet.flow.com/) - [NuFi Wallet](https://nu.fi/) - [Blocto](https://blocto.portto.io/en/) - [Ledger](https://ledger.com) (limited transaction support) - [Dapper Wallet](https://www.meetdapper.com/) (beta access - general availability coming soon) ### Wallet Discovery It can be difficult to get users to discover new wallets on a chain. To solve this, we created a [wallet discovery service](https://github.com/onflow/fcl-discovery) that can be configured and accessed through FCL to display all available Flow wallet providers to the user. This means: - Dapps can display and support all FCL compatible wallets that launch on Flow without needing to change any code - Users don't need to sign up for new wallets - they can carry over their existing one to any dapp that uses FCL for authentication and authorization. The discovery feature can be used via API allowing you to customize your own UI or you can use the default UI without any additional configuration. > Note: To get your wallet added to the discovery service, make a PR in [fcl-discovery](https://github.com/onflow/fcl-discovery). ### Building a FCL compatible wallet - Read the [wallet guide](https://github.com/onflow/fcl-js/blob/master/packages/fcl-core/src/wallet-provider-spec/draft-v4.md) to understand the implementation details. - Review the architecture of the [FCL dev wallet](https://github.com/onflow/fcl-dev-wallet) for an overview. - If building a non-custodial wallet, see the [Account API](https://github.com/onflow/flow-account-api) and the [FLIP](https://github.com/onflow/flow/pull/727) on derivation paths and key generation. --- ## 🛠 Want to Use the Flow SDK Directly? If you prefer to interact with Flow at a **lower level** without using FCL, you can use the [Flow JavaScript SDK](packages/sdk/readme.md) directly. The SDK provides raw access to the Flow Access API for sending transactions, executing scripts, and managing accounts. FCL is built **on top of the Flow SDK**, making it easier to handle authentication, wallet interactions, and dapp connectivity. Choose the approach that best fits your use case. ## Support - Notice a problem or want to request a feature? [Add an issue](https://github.com/onflow/fcl-js/issues). - Join the Flow community on [Discord](https://discord.gg/flow) to keep up to date and to talk to the team. - Read the [Contributing Guide](./CONTRIBUTING.md) to learn how to contribute to the project. ## FAQ **What is FCL?** FCL (Flow Client Library) is a JavaScript and TypeScript package that standardizes how dApps connect to wallets and interact with the Flow network. It handles wallet discovery, authentication, transaction signing, and Cadence script execution. **How is FCL different from the Flow JavaScript SDK?** FCL is built on top of the Flow JavaScript SDK (`@onflow/sdk`). FCL adds wallet interactions, discovery, and higher-level helpers. If you prefer to interact with Flow at a lower level without wallet integrations, you can use the SDK directly. **Which environments does FCL run in?** FCL can run in both browser and server environments. Wallet interactions are browser-only; server-side usage is supported for queries, transactions signed with custom authorizers, and account utilities. **Does FCL support TypeScript?** Yes. Types are available in the [`@onflow/typedefs`](./packages/typedefs) package and exported from the main FCL packages. **What wallets does FCL support?** FCL works with any FCL-compatible wallet, including Flow Wallet, NuFi, Blocto, Ledger, and Dapper Wallet. Discovery is provided via the [wallet discovery service](https://github.com/onflow/fcl-discovery). **Can I use FCL with Flow EVM?** Yes. The repo includes adapters for [RainbowKit](./packages/fcl-rainbowkit-adapter) and [Wagmi](./packages/fcl-wagmi-adapter), plus an [EIP-1193 Ethereum provider](./packages/fcl-ethereum-provider) backed by FCL. **Where do I report bugs or request features?** Open an issue at [github.com/onflow/fcl-js/issues](https://github.com/onflow/fcl-js/issues) or start a conversation on the [Flow Forum](https://forum.flow.com). ## About Flow This repo is part of the [Flow network](https://flow.com), a Layer 1 blockchain built for consumer applications, AI Agents, and DeFi at scale. - Developer docs: https://developers.flow.com - Cadence language: https://cadence-lang.org - Community: [Flow Discord](https://discord.gg/flow) · [Flow Forum](https://forum.flow.com) - Governance: [Flow Improvement Proposals](https://github.com/onflow/flips) ================================================ FILE: SECURITY.md ================================================ # Responsible Disclosure Policy Flow was built from the ground up with security in mind. Our code, infrastructure, and development methodology helps us keep our users safe. We really appreciate the community's help. Responsible disclosure of vulnerabilities helps to maintain the security and privacy of everyone. If you care about making a difference, please follow the guidelines below. # **Guidelines For Responsible Disclosure** We ask that all researchers adhere to these guidelines [here](https://flow.com/flow-responsible-disclosure). ================================================ FILE: docs/flow-docs.json ================================================ { "$schema": "https://developers.flow.com/schemas/flow-docs.json", "displayName": "Flow Client Library (JS)", "headers": { "": { "icon": "fcl-js", "title": "Flow Client Library", "description": "The Flow Client Library (FCL) JS is a package used to interact with user wallets and the Flow blockchain enabling dapps to support all FCL-compatible wallets and users without any custom integrations to the dapp code", "headerCards": [ { "title": "Installation", "tags": ["setup", "guide"], "description": "Set up your local environment and install the necessary dependencies to start using FCL", "href": "/tools/fcl-js#getting-started" }, { "title": "Flow App Quickstart", "tags": ["tutorial", "dapp"], "description": "A tutorial that will allow you to start building with web3 on the Flow blockchain and FCL", "href": "/tools/fcl-js/tutorials/flow-app-quickstart" }, { "title": "Wallet Discovery", "tags": ["wallets", "reference"], "description": "Learn more about integrating Flow compatible wallets with your dapp", "href": "/tools/fcl-js/reference/discovery" } ] } }, "sidebars": { "": [ { "title": "Flow Client Library JS", "items": [ { "title": "Introduction", "href": "" } ] }, { "title": "Tutorials", "items": [ { "title": "Flow App Quickstart", "href": "tutorials/flow-app-quickstart" } ] }, { "title": "Reference", "items": [ { "title": "API", "href": "reference/api" }, { "title": "Authentication", "href": "reference/authentication" }, { "title": "Configuring FCL", "href": "reference/configure-fcl" }, { "title": "Discovery", "href": "reference/discovery" }, { "title": "Interaction Templates", "href": "reference/interaction-templates" }, { "title": "Proving Authentication", "href": "reference/proving-authentication" }, { "title": "Scripts", "href": "reference/scripts" }, { "title": "SDK Guidelines", "href": "reference/sdk-guidelines" }, { "title": "Transactions", "href": "reference/transactions" }, { "title": "User Signatures", "href": "reference/user-signatures" }, { "title": "Add FCL Support for WalletConnect 2.0", "href": "reference/wallet-connect" } ] } ] } } ================================================ FILE: docs/index.md ================================================ # This document has been moved to a new location: https://github.com/onflow/docs/tree/main/docs/tooling/fcl-js/index.md ================================================ FILE: docs/index.mdx ================================================ # This document has been moved to a new location: https://github.com/onflow/docs/tree/main/docs/tooling/fcl-js/index.mdx.txt ================================================ FILE: docs/reference/api.md ================================================ # This document has been moved to a new location: https://github.com/onflow/docs/tree/main/docs/tooling/fcl-js/api.md ================================================ FILE: docs/reference/authentication.mdx ================================================ # This document has been moved to a new location: https://github.com/onflow/docs/tree/main/docs/tooling/fcl-js/authentication.mdx ================================================ FILE: docs/reference/configure-fcl.mdx ================================================ # This document has been moved to a new location: https://github.com/onflow/docs/tree/main/docs/tooling/fcl-js/configure-fcl.mdx ================================================ FILE: docs/reference/discovery.mdx ================================================ # This document has been moved to a new location: https://github.com/onflow/docs/tree/main/docs/tooling/fcl-js/discovery.mdx ================================================ FILE: docs/reference/installation.mdx ================================================ # This document has been moved to a new location: https://github.com/onflow/docs/tree/main/docs/tooling/fcl-js/installation.mdx ================================================ FILE: docs/reference/interaction-templates.mdx ================================================ # This document has been moved to a new location: https://github.com/onflow/docs/tree/main/docs/tooling/fcl-js/interaction-templates.mdx ================================================ FILE: docs/reference/proving-authentication.mdx ================================================ # This document has been moved to a new location: https://github.com/onflow/docs/tree/main/docs/tooling/fcl-js/proving-authentication.mdx ================================================ FILE: docs/reference/scripts.mdx ================================================ # This document has been moved to a new location: https://github.com/onflow/docs/tree/main/docs/tooling/fcl-js/scripts.mdx ================================================ FILE: docs/reference/sdk-guidelines.mdx ================================================ # This document has been moved to a new location: https://github.com/onflow/docs/tree/main/docs/tooling/fcl-js/sdk-guidelines.mdx ================================================ FILE: docs/reference/transactions.mdx ================================================ # This document has been moved to a new location: https://github.com/onflow/docs/tree/main/docs/tooling/fcl-js/transactions.mdx ================================================ FILE: docs/reference/user-signatures.mdx ================================================ # This document has been moved to a new location: https://github.com/onflow/docs/tree/main/docs/tooling/fcl-js/user-signatures.mdx ================================================ FILE: docs/reference/wallet-connect.mdx ================================================ # This document has been moved to a new location: https://github.com/onflow/docs/tree/main/docs/tooling/fcl-js/wallet-connect.mdx ================================================ FILE: docs/tutorials/flow-app-quickstart.mdx ================================================ # This document has been moved to a new location: https://github.com/onflow/docs/tree/main/docs/tutorials/flow-app-quickstart.mdx ================================================ FILE: docs-generator/README.md ================================================ # Docs Generator This directory contains scripts to generate documentation for Flow Client Library (FCL) packages. ## Overview The documentation generator creates Markdown files for Docusaurus v2 websites. It automatically extracts TypeScript function signatures, parameter types, return types, and JSDoc comments to create comprehensive API documentation. ## Directory Structure - `generate-docs.js` - Main script for generating documentation for a single package - `generate-all-docs.js` - Script to generate documentation for all packages with the generate-docs script - `templates/` - Handlebars templates for generating documentation pages - `output/` - Where generated files are created - `generators/` - Function utils to generate pages from templates and data ## Features - Automatic discovery of packages with generate-docs scripts - Support for JSDoc comments including function descriptions, parameter info, and usage examples - Handlebars templates for easy customization of output format - Consistent documentation structure across all packages - Package index page listing all available packages - Core types documentation from the @onflow/typedefs package ### JSDoc Support The documentation generator extracts information from JSDoc comments in code. JSDoc comments can be added to improve the generated documentation: ```javascript /** * This description will be used in the documentation. * * @param {string} param1 - This description will be used for the parameter * @returns {number} This description will be used for the return value * @example * // This example will be used in the documentation * const result = myFunction("test") */ export function myFunction(param1) { // ... } ``` ## Usage ### For Individual Packages Each package that needs documentation should include a `generate-docs` script in its `package.json`: ```json { "scripts": { "generate-docs": "node ../../docs-generator/generate-docs.js" } } ``` To generate documentation for a single package, run: ```bash cd packages/<package-name> npm run generate-docs ``` ### For All Packages To generate documentation for all packages that have a `generate-docs` script: ```bash npm run generate-docs-all ``` This will: 1. Find all packages with a `generate-docs` script 2. Run the script for each package 3. Generate documentation in the `/docs-generator/output/packages-docs/<package-name>` directory 4. Generate core types documentation in `/docs-generator/output/packages-docs/types/index.md` ## Output Structure The generated documentation follows this structure: ``` /docs-generator/output/ └── packages-docs/ # Main folder containing ├── package-a/ # Documentation for package-a │ ├── index.md # Main package page with installation instructions and API │ ├── functionName1.md │ ├── functionName2.md │ └── ... ├── package-b/ ├── types/ │ └── index.md # Type definitions page with interfaces, type aliases, and enums └── index.md # List contents of the folder ``` Each package has a main page that includes: - Package overview - Installation instructions - API reference with links to individual function documentation ### Auto-generation Notes All generated files are automatically generated from the source code of FCL packages and are ignored by git (except this README). Do not modify these files directly as they will be overwritten when documentation is regenerated. Instead: - Update the JSDoc comments in the source code - Customize the templates in `docs-generator/templates/` - Create a `docs-generator.config.js` file in the package root for custom content ## Customizing Templates You can customize the generated documentation by editing the Handlebars templates in the `templates/` directory. ### Custom Package Documentation Packages can provide custom documentation content by creating a `docs-generator.config.js` file in the package root directory. The following customizations are supported: ```js module.exports = { customData: { displayName: `Custom Package Reference`, // Used for Docusaurus sidebar title sections: { overview: ``, // Custom overview section requirements: ``, // Custom requirements section importing: ``, // Custom importing section }, extra: ``, // Additional content }, }; ``` All properties in the configuration are optional. If a property is not specified, default values will be used. ## Adding Documentation to a New Package To add documentation generation to a new package: 1. Add the generate-docs script to the package's `package.json`: ```json { "scripts": { "generate-docs": "node ../../docs-generator/generate-docs.js" } } ``` 2. Ensure the code has proper JSDoc comments for better documentation. 3. Run the generate-docs script to test it. This package will now be included when running the `generate-docs-all` command. ## Core Types Documentation The generator also creates documentation for all types, interfaces, and enums exported from the `@onflow/typedefs` package. This documentation is generated every time you run the generate-docs script for any package, ensuring that the types documentation is always up-to-date. The types documentation in the `types` directory includes: - **Interfaces** - Documented with their properties and methods - **Types** - Documented with their underlying types - **Enums** - Documented with all their members and values All type documentation includes JSDoc descriptions when available. ## Integration with Documentation Projects After generating documentation, copy the `output/packages-docs` directory to the documentation project. This will maintain the folder structure and allow the documentation build system to process the files. ## Notes - Avoid relative path linking outside of packages-docs folder to avoid docusaurus linking problems. Use only packages-docs relative links or absolute paths. - If adding an example to a jsdoc, avoid adding backticks, it will be directly embedded into typescript backticks on pages generation. - Don't use multiple @returns in jsdocs, they are not supported. ================================================ FILE: docs-generator/generate-all-docs.js ================================================ const fs = require("fs") const path = require("path") const {execSync} = require("child_process") async function main() { try { // Get packages source directory const sourcePackagesDir = path.resolve(__dirname, "../packages") // Find packages with generate-docs script console.log(`Scanning for packages in ${sourcePackagesDir}`) const packages = fs.readdirSync(sourcePackagesDir).filter(name => { try { const itemPath = path.join(sourcePackagesDir, name) // Check if it's a directory first if (!fs.statSync(itemPath).isDirectory()) { return false } const packageJsonPath = path.join( sourcePackagesDir, name, "package.json" ) const packageJson = JSON.parse( fs.readFileSync(packageJsonPath, "utf8") ) return packageJson.scripts && packageJson.scripts["generate-docs"] } catch (error) { console.warn(`Error checking package ${name}: ${error.message}`) return false } }) || [] if (packages.length === 0) { console.warn("No packages with generate-docs script were found.") return } console.log(`Found ${packages.length} packages with generate-docs script:`) packages.forEach(pkg => console.log(`- ${pkg}`)) // Navigate to the package directory and run the generate-docs script for (const pkg of packages) { const pkgDir = path.join(sourcePackagesDir, pkg) execSync(`cd ${pkgDir} && npm run generate-docs`, { stdio: "inherit", env: {...process.env}, }) console.log("") } // Report results console.log(`All docs correctly generated.`) } catch (error) { console.error("Error:") console.error(error.message || error) process.exit(1) } } main().catch(error => { console.error("Unhandled error:") console.error(error.message || error) process.exit(1) }) ================================================ FILE: docs-generator/generate-docs.js ================================================ const fs = require("fs") const path = require("path") const {Project} = require("ts-morph") const Handlebars = require("handlebars") const { generateRootPage, generatePackagePage, generateFunctionPage, generateTypesPage, generateNamespacePage, } = require("./generators") const { discoverWorkspacePackages, extractExportsFromEntryFile, } = require("./utils") async function main() { try { // Extract package name from the name field of the package where the command is run (@onflow/fcl -> fcl) const packageJson = JSON.parse( fs.readFileSync(path.resolve(process.cwd(), "package.json"), "utf8") ) const packageName = packageJson.name.split("/").pop() console.log(`Generating docs for ${packageName}...`) // Get the entry file from package.json source field const entryFile = packageJson.source || "" const ENTRY_FILE_PATH = path.resolve(process.cwd(), entryFile) // Configuration with updated directory structure const TEMPLATES_DIR = path.resolve(__dirname, "./templates") const OUTPUT_DIR = path.resolve(__dirname, "./output") const ROOT_DIR = path.join(OUTPUT_DIR, "packages-docs") const PACKAGE_DIR = path.join(ROOT_DIR, packageName) const TYPES_DIR = path.join(ROOT_DIR, "types") // Ensure output directories exist await fs.promises.mkdir(OUTPUT_DIR, {recursive: true}) await fs.promises.mkdir(ROOT_DIR, {recursive: true}) // Clean existing output directory content for the package before creating its folder await fs.promises.rm(PACKAGE_DIR, {recursive: true, force: true}) await fs.promises.mkdir(PACKAGE_DIR, {recursive: true}) await fs.promises.mkdir(TYPES_DIR, {recursive: true}) // Handlebars templates to be used for generating the docs const templates = { root: Handlebars.compile( fs.readFileSync(path.join(TEMPLATES_DIR, "root.hbs"), "utf8") ), package: Handlebars.compile( fs.readFileSync(path.join(TEMPLATES_DIR, "package.hbs"), "utf8") ), function: Handlebars.compile( fs.readFileSync(path.join(TEMPLATES_DIR, "function.hbs"), "utf8") ), types: Handlebars.compile( fs.readFileSync(path.join(TEMPLATES_DIR, "types.hbs"), "utf8") ), namespace: Handlebars.compile( fs.readFileSync(path.join(TEMPLATES_DIR, "namespace.hbs"), "utf8") ), } // Initialize ts-morph project and add source files const project = new Project({ skipAddingFilesFromTsConfig: true, }) // Add the entry file project.addSourceFileAtPath(ENTRY_FILE_PATH) // Automatically discover and add all workspace packages for resolving imports const workspacePackagePaths = discoverWorkspacePackages() for (const packagePath of workspacePackagePaths) { try { project.addSourceFilesAtPaths(packagePath) } catch (e) { console.warn( `Could not add source files from ${packagePath}: ${e.message}` ) } } // Get the entry source file and extract exports from it const entrySourceFile = project.getSourceFile(ENTRY_FILE_PATH) const {functions, namespaces} = extractExportsFromEntryFile(entrySourceFile) console.log( `Found ${functions.length} functions and ${namespaces.length} namespaces` ) // Collect all namespace functions for the package index let allNamespaceFunctions = [] namespaces.forEach(namespace => { allNamespaceFunctions = allNamespaceFunctions.concat(namespace.functions) }) // Generate documentation generateRootPage(templates, ROOT_DIR, packageName) generatePackagePage( templates, PACKAGE_DIR, packageName, functions, namespaces, allNamespaceFunctions ) // Generate function pages for regular functions only functions.forEach(func => { generateFunctionPage(templates, PACKAGE_DIR, packageName, func) }) // Generate single namespace pages (no individual function pages) namespaces.forEach(namespace => { generateNamespacePage(templates, PACKAGE_DIR, packageName, namespace) }) // Generate the types documentation generateTypesPage(templates, TYPES_DIR) console.log(`Docs generated correctly for ${packageName}.`) return true } catch (error) { console.error("Error generating docs:") console.error(error.message) return false } } main().catch(error => { console.error("Unhandled error:") console.error(error.message || error) process.exit(1) }) ================================================ FILE: docs-generator/generators/generate-function-page.js ================================================ const path = require("path") const {Project} = require("ts-morph") const fs = require("fs") const {generatePage, getFirstWord} = require("./utils") const {stripGenericParams} = require("../utils/type-utils") const {formatTypeScript} = require("../utils/typescript-formatter") // Cache for type structures to avoid repeated processing const typeStructureCache = new Map() function getGenericTypeStructure(baseTypeName, packageName, sourceFilePath) { // Input validation if (!baseTypeName || typeof baseTypeName !== "string") { return null } // Check cache first const cacheKey = `${packageName}:${baseTypeName}` if (typeStructureCache.has(cacheKey)) { return typeStructureCache.get(cacheKey) } let result = null try { const project = new Project({skipAddingFilesFromTsConfig: true}) // Only add necessary source files const sourcePaths = [] if (sourceFilePath) { const fullSourcePath = path.resolve(process.cwd(), "../", sourceFilePath) if (fs.existsSync(fullSourcePath)) { sourcePaths.push(fullSourcePath) } } if (packageName) { const packageSrcDir = path.resolve( process.cwd(), "../", packageName, "src" ) if (fs.existsSync(packageSrcDir)) { sourcePaths.push(`${packageSrcDir}/**/*.ts`) } } // Add source files in batch if (sourcePaths.length > 0) { try { project.addSourceFilesAtPaths(sourcePaths) } catch (e) { // If batch add fails, try individually sourcePaths.forEach(sourcePath => { try { if (!sourcePath.includes("*")) { project.addSourceFileAtPath(sourcePath) } else { project.addSourceFilesAtPaths(sourcePath) } } catch (e) { // Skip problematic source paths } }) } } // Find the type definition efficiently const sourceFiles = project.getSourceFiles() for (const sourceFile of sourceFiles) { const typeAlias = sourceFile.getTypeAlias(baseTypeName) if (typeAlias) { result = extractTypeStructure(typeAlias) if (result) break // Stop on first successful extraction } } } catch (e) { // Fail silently to avoid breaking the docs generation } // Cache the result (including null results to avoid repeated failures) typeStructureCache.set(cacheKey, result) return result } function extractTypeStructure(typeAlias) { try { const typeNode = typeAlias.getTypeNode() if (!typeNode || typeof typeNode.getProperties !== "function") { return null } const properties = typeNode.getProperties() if (properties.length === 0) { return null } const structure = {} for (const prop of properties) { try { const propName = prop.getName() if (!propName) continue const propType = prop.getType() const cleanPropType = propType ? extractTypeName(propType.getText()) : "unknown" // Get JSDoc comments efficiently let propDescription = "" const jsDocComments = prop.getJsDocs() if (jsDocComments?.length > 0) { propDescription = jsDocComments[0].getDescription()?.trim() || "" } structure[propName] = { type: cleanPropType, description: propDescription, optional: prop.hasQuestionToken?.() || false, } } catch (e) { // Skip problematic properties but continue processing others continue } } return Object.keys(structure).length > 0 ? structure : null } catch (e) { return null } } // Basic primitive types that don't need definitions const PRIMITIVE_TYPES = new Set([ "string", "number", "boolean", "object", "any", "void", "unknown", "never", "null", "undefined", ]) // Cache for type definitions and core types to avoid repeated lookups const typeCache = new Map() const coreTypesCache = new Set() let hasLoadedCoreTypes = false // Function to escape MDX-sensitive characters in example code function escapeMDXCharacters(text) { if (!text) return text // Handle already escaped curly braces by converting them to inline code // This pattern matches things like A.\{AccountAddress\}.\{ContractName\}.\{EventName\} text = text.replace(/A\.\\{[^}]+\\}\.\\{[^}]+\\}\.\\{[^}]+\\}/g, match => { // Remove the backslashes and wrap in backticks const cleanMatch = match.replace(/\\/g, "") return `\`${cleanMatch}\`` }) // Handle other patterns of escaped curly braces by converting to inline code text = text.replace(/\\{[^}]+\\}/g, match => { // Remove the backslashes and wrap in backticks const cleanMatch = match.replace(/\\/g, "") return `\`${cleanMatch}\`` }) // Split text by both multi-line code blocks (triple backticks) and inline code (single backticks) // This regex captures both patterns while preserving them const parts = text.split(/(```[\s\S]*?```|`[^`\n]*`)/g) return parts .map((part, index) => { // Check if this part is a code block (either multi-line or inline) const isCodeBlock = part.startsWith("```") || (part.startsWith("`") && part.endsWith("`") && !part.includes("\n")) if (isCodeBlock) { // Don't escape anything inside code blocks (both multi-line and inline) return part } else { // Escape curly braces only outside code blocks return part.replace(/(?<!\\)\{/g, "\\{").replace(/(?<!\\)\}/g, "\\}") } }) .join("") } function extractCoreTypes() { if (hasLoadedCoreTypes) return coreTypesCache try { const typedefsSrcDir = path.join( path.resolve(process.cwd(), "../typedefs"), "src" ) const project = new Project({skipAddingFilesFromTsConfig: true}) project.addSourceFilesAtPaths(`${typedefsSrcDir}/**/*.ts`) project.getSourceFiles().forEach(sourceFile => { sourceFile.getInterfaces().forEach(iface => { if (iface.isExported()) coreTypesCache.add(iface.getName()) }) sourceFile.getTypeAliases().forEach(typeAlias => { if (typeAlias.isExported()) coreTypesCache.add(typeAlias.getName()) }) sourceFile.getEnums().forEach(enumDef => { if (enumDef.isExported()) coreTypesCache.add(enumDef.getName()) }) }) hasLoadedCoreTypes = true return coreTypesCache } catch (error) { console.warn(`Error extracting core types: ${error.message}`) return new Set() } } function extractTypeName(fullType) { if (!fullType) return fullType // Clean up import references and comments let cleanType = fullType .replace(/import\([^)]+\)\./g, "") .replace(/\/\*[\s\S]*?\*\//g, "") .trim() // For union types, preserve the structure if (cleanType.includes("|")) { return cleanType } // For simple types without complex structures if ( !cleanType.includes("Promise<") && !cleanType.includes("=>") && !cleanType.includes("{") ) { return cleanType } // Handle Promise types if (cleanType.startsWith("Promise<")) { const innerType = cleanType.match(/Promise<(.+)>/) if (innerType && innerType[1]) { return `Promise<${extractTypeName(innerType[1])}>` } } // Handle function types if (cleanType.includes("=>")) { return cleanType } // Handle array types if (cleanType.endsWith("[]")) { return `${extractTypeName(cleanType.slice(0, -2))}[]` } // Handle complex objects - return as is for now if (cleanType.includes("{") && cleanType.includes("}")) { return cleanType } return cleanType } // Check if a type name exists in the typedefs package function isTypeInTypedefs(typeName, coreTypes) { // Handle Promise<Type> - extract the inner type if (typeName.startsWith("Promise<") && typeName.endsWith(">")) { const innerType = typeName.slice(8, -1).trim() // Remove Promise< and > return coreTypes.has(innerType) } // Handle Array types - extract the base type if (typeName.endsWith("[]")) { const baseType = typeName.slice(0, -2).trim() return coreTypes.has(baseType) } // Handle union types - check if any part is in typedefs if (typeName.includes("|")) { const types = typeName.split("|").map(t => t.trim()) return types.some(t => coreTypes.has(t)) } return coreTypes.has(typeName) } // Check if a type is non-primitive (interface, type alias, or arrow function) function isNonPrimitiveType(typeString) { if (!typeString || PRIMITIVE_TYPES.has(typeString)) { return false } // Remove whitespace for better pattern matching const cleanType = typeString.trim() // Function types (arrow functions) if (cleanType.includes("=>")) { return true } // Object types with properties (inline object types) if (cleanType.includes("{") && cleanType.includes(":")) { return true } // Complex union types (more than just primitive types) if (cleanType.includes("|")) { const unionTypes = cleanType.split("|").map(t => t.trim()) // If any part of the union is not primitive, show definition const hasNonPrimitive = unionTypes.some( t => !PRIMITIVE_TYPES.has(t) && !t.match(/^(null|undefined)$/) && (t.match(/^[A-Z][a-zA-Z0-9]*$/) || t.includes("=>") || t.includes("{")) ) if (hasNonPrimitive) return true } // Generic types like Promise<SomeType>, Array<CustomType>, etc. if (cleanType.includes("<") && cleanType.includes(">")) { // Extract the inner type from generics const genericMatch = cleanType.match( /^([A-Za-z][a-zA-Z0-9]*)<(.+)>(\[\])?$/ ) if (genericMatch) { const [, outerType, innerType] = genericMatch // If outer type is Promise, Array, etc., check if inner type is non-primitive if (["Promise", "Array"].includes(outerType)) { return isNonPrimitiveType(innerType.trim()) } // For other generic types, if the outer type starts with uppercase, it's likely custom if (/^[A-Z]/.test(outerType)) { return true } } return true // Any other generic type is likely non-primitive } // Tuple types if ( cleanType.startsWith("[") && cleanType.endsWith("]") && cleanType.includes(",") ) { return true } // Mapped types or conditional types if ( cleanType.includes("keyof") || cleanType.includes("extends") || cleanType.includes("infer") ) { return true } // Array types with custom base types if (cleanType.endsWith("[]")) { const baseType = cleanType.slice(0, -2).trim() return isNonPrimitiveType(baseType) } // Custom type names (PascalCase starting with uppercase) if (/^[A-Z][a-zA-Z0-9]*$/.test(cleanType)) { return true } // Type names with namespaces (e.g., FCL.SomeType) if (/^[A-Z][a-zA-Z0-9]*\.[A-Z][a-zA-Z0-9]*$/.test(cleanType)) { return true } // Function types in different formats if (cleanType.match(/^\(.+\)\s*=>/)) { return true } // Constructor types if (cleanType.startsWith("new ") && cleanType.includes("=>")) { return true } return false } function extractTypeDefinition(sourceFile, node) { if (!node) return null const text = sourceFile.getFullText().substring(node.getPos(), node.getEnd()) // Remove comments and extra whitespace return text .replace(/^[\r\n\s]+/, "") // Trim leading whitespace .replace(/[\r\n\s]+$/, "") // Trim trailing whitespace .replace(/\/\*\*[\s\S]*?\*\//g, "") // Remove JSDoc comments .replace(/\/\*[\s\S]*?\*\//g, "") // Remove multi-line comments .replace(/\/\/.*$/gm, "") // Remove single-line comments .replace(/\n\s*\n+/g, "\n") // Replace multiple blank lines with a single one .trim() } function findTypeInFile(sourceFile, typeName) { // Check for interface const iface = sourceFile.getInterface(typeName) if (iface) return extractTypeDefinition(sourceFile, iface) // Check for type alias const typeAlias = sourceFile.getTypeAlias(typeName) if (typeAlias) return extractTypeDefinition(sourceFile, typeAlias) // Check for enum const enumDef = sourceFile.getEnum(typeName) if (enumDef) return extractTypeDefinition(sourceFile, enumDef) return null } function getTypeDefinition(typeName, packageName, sourceFilePath) { if (!typeName || PRIMITIVE_TYPES.has(typeName)) { return null } // Handle Promise and array types - extract base type let baseTypeName = typeName if (baseTypeName.startsWith("Promise<")) { const match = baseTypeName.match(/Promise<(.+)>/) if (match) baseTypeName = match[1] } if (baseTypeName.endsWith("[]")) { baseTypeName = baseTypeName.slice(0, -2) } // Check cache first const cacheKey = `${packageName}:${baseTypeName}` if (typeCache.has(cacheKey)) { return typeCache.get(cacheKey) } let definition = null try { // Create a new project for type searching to avoid conflicts const project = new Project({skipAddingFilesFromTsConfig: true}) // First check source file if provided if (sourceFilePath) { const fullSourcePath = path.resolve(process.cwd(), "../", sourceFilePath) if (fs.existsSync(fullSourcePath)) { const sourceFile = project.addSourceFileAtPath(fullSourcePath) definition = findTypeInFile(sourceFile, baseTypeName) // If not found in the source file, check its imports and re-exports if (!definition) { definition = searchTypeInImports(sourceFile, baseTypeName, project) } } } // If not found, search package src directory if (!definition) { const packageSrcDir = path.resolve( process.cwd(), "../", packageName, "src" ) if (fs.existsSync(packageSrcDir)) { // Add all TypeScript files in the package project.addSourceFilesAtPaths(`${packageSrcDir}/**/*.ts`) // Search through all source files in the package for (const sourceFile of project.getSourceFiles()) { definition = findTypeInFile(sourceFile, baseTypeName) if (definition) break } } } // If still not found, search in common Flow workspace packages if (!definition) { definition = searchTypeInWorkspacePackages(baseTypeName, project) } // Cache the result typeCache.set(cacheKey, definition) return definition } catch (error) { console.warn( `Error getting type definition for ${typeName}: ${error.message}` ) return null } } // Helper function to search for types in imports and re-exports function searchTypeInImports(sourceFile, typeName, project) { try { // Check import declarations const importDeclarations = sourceFile.getImportDeclarations() for (const importDecl of importDeclarations) { const namedImports = importDecl.getNamedImports() const hasImport = namedImports.some( namedImport => namedImport.getName() === typeName ) if (hasImport) { const moduleSpecifier = importDecl.getModuleSpecifier() if (moduleSpecifier) { const moduleSpecValue = moduleSpecifier.getLiteralValue() const resolvedFile = resolveImportPath( sourceFile, moduleSpecValue, project ) if (resolvedFile) { const definition = findTypeInFile(resolvedFile, typeName) if (definition) return definition } } } } // Check export declarations (re-exports) const exportDeclarations = sourceFile.getExportDeclarations() for (const exportDecl of exportDeclarations) { const namedExports = exportDecl.getNamedExports() const hasExport = namedExports.some( namedExport => namedExport.getName() === typeName ) if (hasExport) { const moduleSpecifier = exportDecl.getModuleSpecifier() if (moduleSpecifier) { const moduleSpecValue = moduleSpecifier.getLiteralValue() const resolvedFile = resolveImportPath( sourceFile, moduleSpecValue, project ) if (resolvedFile) { const definition = findTypeInFile(resolvedFile, typeName) if (definition) return definition // Recursively search in the resolved file's imports const recursiveDefinition = searchTypeInImports( resolvedFile, typeName, project ) if (recursiveDefinition) return recursiveDefinition } } } } return null } catch (error) { console.warn(`Error searching type in imports: ${error.message}`) return null } } // Helper function to resolve import paths function resolveImportPath(sourceFile, moduleSpecifier, project) { try { // Handle @onflow/ package imports if (moduleSpecifier.startsWith("@onflow/")) { const packageName = moduleSpecifier.replace("@onflow/", "") const packageDir = path.resolve(process.cwd(), "../", packageName) const packageJsonPath = path.join(packageDir, "package.json") if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) const entryFile = packageJson.source || packageJson.main || "src/index.ts" const entryFilePath = path.resolve(packageDir, entryFile) if (fs.existsSync(entryFilePath)) { let targetFile = project.getSourceFile(entryFilePath) if (!targetFile) { targetFile = project.addSourceFileAtPath(entryFilePath) } return targetFile } } } // Handle relative imports else if ( moduleSpecifier.startsWith("./") || moduleSpecifier.startsWith("../") ) { const sourceDir = path.dirname(sourceFile.getFilePath()) const resolvedPath = path.resolve(sourceDir, moduleSpecifier) // Try different extensions const possiblePaths = [ resolvedPath + ".ts", resolvedPath + ".d.ts", resolvedPath + "/index.ts", resolvedPath + "/index.d.ts", ] for (const possiblePath of possiblePaths) { if (fs.existsSync(possiblePath)) { let targetFile = project.getSourceFile(possiblePath) if (!targetFile) { targetFile = project.addSourceFileAtPath(possiblePath) } return targetFile } } } return null } catch (error) { console.warn( `Error resolving import path ${moduleSpecifier}: ${error.message}` ) return null } } // Helper function to search for types in common workspace packages function searchTypeInWorkspacePackages(typeName, project) { try { const workspacePackages = ["sdk", "typedefs", "fcl-core", "types"] for (const packageName of workspacePackages) { const packageSrcDir = path.resolve( process.cwd(), "../", packageName, "src" ) if (fs.existsSync(packageSrcDir)) { try { project.addSourceFilesAtPaths(`${packageSrcDir}/**/*.ts`) for (const sourceFile of project.getSourceFiles()) { if (sourceFile.getFilePath().includes(packageName)) { const definition = findTypeInFile(sourceFile, typeName) if (definition) return definition } } } catch (e) { // Continue to next package if this one fails console.warn( `Could not search in package ${packageName}: ${e.message}` ) } } } return null } catch (error) { console.warn(`Error searching workspace packages: ${error.message}`) return null } } function processTypeForDisplay( typeString, coreTypes, packageName, sourceFilePath ) { if (!typeString) { return { displayType: typeString, hasLink: false, linkedType: null, typeDefinition: null, } } const extractedType = extractTypeName(typeString) // Handle union types specially if (extractedType.includes("|")) { const unionTypes = extractedType.split("|").map(t => t.trim()) let hasAnyLink = false const processedTypes = unionTypes.map(type => { if (isTypeInTypedefs(type, coreTypes)) { hasAnyLink = true let linkType = type let linkFragment = type.toLowerCase() // Handle Promise<Type> - link to the inner type if (type.startsWith("Promise<") && type.endsWith(">")) { const innerType = type.slice(8, -1).trim() linkFragment = innerType.toLowerCase() } // Handle Array types if (type.endsWith("[]")) { const baseType = type.slice(0, -2).trim() linkFragment = baseType.toLowerCase() } return `[\`${type}\`](../types#${linkFragment})` } else { return `\`${type}\`` } }) if (hasAnyLink) { return { displayType: extractedType, hasLink: true, linkedType: processedTypes.join(" | "), typeDefinition: null, } } } // Check if type exists in typedefs package if (isTypeInTypedefs(extractedType, coreTypes)) { let linkType = extractedType let linkFragment = extractedType.toLowerCase() // Handle Promise<Type> - link to the inner type if (extractedType.startsWith("Promise<") && extractedType.endsWith(">")) { const innerType = extractedType.slice(8, -1).trim() linkFragment = innerType.toLowerCase() } // Handle Array types if (extractedType.endsWith("[]")) { const baseType = extractedType.slice(0, -2).trim() linkFragment = baseType.toLowerCase() } // Handle TypeScript utility types - link to the inner type const utilityTypeMatch = extractedType.match( /^(Partial|Required|Omit|Pick|Record|Exclude|Extract|NonNullable|ReturnType|Parameters|ConstructorParameters|InstanceType|ThisParameterType|OmitThisParameter|ThisType)<(.+)>$/ ) if (utilityTypeMatch) { const [, utilityType, innerType] = utilityTypeMatch const firstTypeParam = innerType.split(",")[0].trim() linkFragment = firstTypeParam.toLowerCase() } return { displayType: extractedType, hasLink: true, linkedType: `[\`${extractedType}\`](../types#${linkFragment})`, typeDefinition: null, } } // Check if it's a non-primitive type that should have a definition shown if (isNonPrimitiveType(extractedType)) { const typeDefinition = getTypeDefinition( extractedType, packageName, sourceFilePath ) // If we found a definition, use it if (typeDefinition) { return { displayType: extractedType, hasLink: false, linkedType: null, typeDefinition: typeDefinition, } } // If no definition found but it's clearly a function type, show it anyway if (extractedType.includes("=>")) { return { displayType: extractedType, hasLink: false, linkedType: null, typeDefinition: formatTypeScript(extractedType), } } // If no definition found but it looks like an interface/type name, // don't show a fallback message - just show nothing if (/^[A-Z][a-zA-Z0-9]*$/.test(extractedType)) { return { displayType: extractedType, hasLink: false, linkedType: null, typeDefinition: null, } } // For other complex types, show them as type definitions return { displayType: extractedType, hasLink: false, linkedType: null, typeDefinition: formatTypeScript(extractedType), } } // For primitive types or simple types, just show the type name return { displayType: extractedType, hasLink: false, linkedType: null, typeDefinition: null, } } function generateFunctionPage(templates, outputDir, packageName, func) { const coreTypes = extractCoreTypes() // Escape MDX characters in description if (func.description) { func.description = escapeMDXCharacters(func.description) } // Process parameters func.parameters = func.parameters.map(param => { const typeInfo = processTypeForDisplay( param.type, coreTypes, packageName, func.sourceFilePath ) // Check if this is a generic type and try to expand it let expandedType = null if (param.type.includes("<") && param.type.includes(">")) { const baseTypeName = stripGenericParams(param.type) // Try to get the structure - if the type doesn't exist, it will return null expandedType = getGenericTypeStructure( baseTypeName, packageName, func.sourceFilePath ) } // Escape MDX characters in parameter description const description = param.description ? escapeMDXCharacters(param.description) : param.description return { ...param, type: typeInfo.displayType, description, linkedType: typeInfo.linkedType, hasLink: typeInfo.hasLink, typeDefinition: typeInfo.typeDefinition, expandedType: expandedType, } }) // Process return type const returnTypeInfo = processTypeForDisplay( func.returnType, coreTypes, packageName, func.sourceFilePath ) func.returnType = returnTypeInfo.displayType func.returnHasLink = returnTypeInfo.hasLink func.linkedType = returnTypeInfo.linkedType func.returnTypeDefinition = returnTypeInfo.typeDefinition // Generate the page directly in the package folder instead of in a reference subfolder const filename = func.name.charAt(0).toLowerCase() + func.name.slice(1) generatePage(templates, "function", path.join(outputDir, `${filename}.md`), { ...func, packageName, packageFirstWord: getFirstWord(packageName), }) } module.exports = {generateFunctionPage} ================================================ FILE: docs-generator/generators/generate-namespace-page.js ================================================ const path = require("path") const fs = require("fs") const {Project} = require("ts-morph") const {generatePage, getFirstWord} = require("./utils") const {formatTypeScript} = require("../utils/typescript-formatter") // Basic primitive types that don't need definitions const PRIMITIVE_TYPES = new Set([ "string", "number", "boolean", "object", "any", "void", "unknown", "never", "null", "undefined", ]) const typeCache = new Map() const coreTypesCache = new Set() let hasLoadedCoreTypes = false // Function to escape MDX-sensitive characters in example code function escapeMDXCharacters(text) { if (!text) return text // Handle already escaped curly braces by converting them to inline code // This pattern matches things like A.\{AccountAddress\}.\{ContractName\}.\{EventName\} text = text.replace(/A\.\\{[^}]+\\}\.\\{[^}]+\\}\.\\{[^}]+\\}/g, match => { // Remove the backslashes and wrap in backticks const cleanMatch = match.replace(/\\/g, "") return `\`${cleanMatch}\`` }) // Handle other patterns of escaped curly braces by converting to inline code text = text.replace(/\\{[^}]+\\}/g, match => { // Remove the backslashes and wrap in backticks const cleanMatch = match.replace(/\\/g, "") return `\`${cleanMatch}\`` }) // Split text by both multi-line code blocks (triple backticks) and inline code (single backticks) // This regex captures both patterns while preserving them const parts = text.split(/(```[\s\S]*?```|`[^`\n]*`)/g) return parts .map((part, index) => { // Check if this part is a code block (either multi-line or inline) const isCodeBlock = part.startsWith("```") || (part.startsWith("`") && part.endsWith("`") && !part.includes("\n")) if (isCodeBlock) { // Don't escape anything inside code blocks (both multi-line and inline) return part } else { // Escape curly braces only outside code blocks return part.replace(/(?<!\\)\{/g, "\\{").replace(/(?<!\\)\}/g, "\\}") } }) .join("") } function extractCoreTypes() { if (hasLoadedCoreTypes) return coreTypesCache try { const typedefsSrcDir = path.join( path.resolve(process.cwd(), "../typedefs"), "src" ) const project = new Project({skipAddingFilesFromTsConfig: true}) project.addSourceFilesAtPaths(`${typedefsSrcDir}/**/*.ts`) project.getSourceFiles().forEach(sourceFile => { sourceFile.getInterfaces().forEach(iface => { if (iface.isExported()) coreTypesCache.add(iface.getName()) }) sourceFile.getTypeAliases().forEach(typeAlias => { if (typeAlias.isExported()) coreTypesCache.add(typeAlias.getName()) }) sourceFile.getEnums().forEach(enumDef => { if (enumDef.isExported()) coreTypesCache.add(enumDef.getName()) }) }) hasLoadedCoreTypes = true return coreTypesCache } catch (error) { console.warn(`Error extracting core types: ${error.message}`) return new Set() } } function extractTypeName(fullType) { if (!fullType) return fullType // Clean up import references and comments let cleanType = fullType .replace(/import\([^)]+\)\./g, "") .replace(/\/\*[\s\S]*?\*\//g, "") .trim() // For union types, preserve the structure if (cleanType.includes("|")) { return cleanType } // For simple types without complex structures if ( !cleanType.includes("Promise<") && !cleanType.includes("=>") && !cleanType.includes("{") ) { return cleanType } // Handle Promise types if (cleanType.startsWith("Promise<")) { const innerType = cleanType.match(/Promise<(.+)>/) if (innerType && innerType[1]) { return `Promise<${extractTypeName(innerType[1])}>` } } // Handle function types if (cleanType.includes("=>")) { return cleanType } // Handle array types if (cleanType.endsWith("[]")) { return `${extractTypeName(cleanType.slice(0, -2))}[]` } // Handle complex objects - return as is for now if (cleanType.includes("{") && cleanType.includes("}")) { return cleanType } return cleanType } // Check if a type name exists in the typedefs package function isTypeInTypedefs(typeName, coreTypes) { // Handle Promise<Type> - extract the inner type if (typeName.startsWith("Promise<") && typeName.endsWith(">")) { const innerType = typeName.slice(8, -1).trim() // Remove Promise< and > return coreTypes.has(innerType) } // Handle Array types - extract the base type if (typeName.endsWith("[]")) { const baseType = typeName.slice(0, -2).trim() return coreTypes.has(baseType) } // Handle union types - check if any part is in typedefs if (typeName.includes("|")) { const types = typeName.split("|").map(t => t.trim()) return types.some(t => coreTypes.has(t)) } return coreTypes.has(typeName) } // Check if a type is non-primitive (interface, type alias, or arrow function) function isNonPrimitiveType(typeString) { if (!typeString || PRIMITIVE_TYPES.has(typeString)) { return false } // Function types (arrow functions) if (typeString.includes("=>")) { return true } // Object types with properties if (typeString.includes("{") && typeString.includes(":")) { return true } // Complex union types if (typeString.includes("|") && typeString.length > 20) { return true } // If it's not a primitive type and is a single word (likely an interface/type alias name) // that starts with uppercase (TypeScript convention), it's probably a custom type if (/^[A-Z][a-zA-Z0-9]*(\[\])?$/.test(typeString)) { return true } // Generic types like Promise<SomeType> where SomeType is not primitive if (typeString.startsWith("Promise<") && typeString.endsWith(">")) { const innerType = typeString.slice(8, -1).trim() return isNonPrimitiveType(innerType) } return false } function extractTypeDefinition(sourceFile, node) { if (!node) return null const text = sourceFile.getFullText().substring(node.getPos(), node.getEnd()) // Remove comments and extra whitespace return text .replace(/^[\r\n\s]+/, "") // Trim leading whitespace .replace(/[\r\n\s]+$/, "") // Trim trailing whitespace .replace(/\/\*\*[\s\S]*?\*\//g, "") // Remove JSDoc comments .replace(/\/\*[\s\S]*?\*\//g, "") // Remove multi-line comments .replace(/\/\/.*$/gm, "") // Remove single-line comments .replace(/\n\s*\n+/g, "\n") // Replace multiple blank lines with a single one .trim() } function findTypeInFile(sourceFile, typeName) { // Check for interface const iface = sourceFile.getInterface(typeName) if (iface) return extractTypeDefinition(sourceFile, iface) // Check for type alias const typeAlias = sourceFile.getTypeAlias(typeName) if (typeAlias) return extractTypeDefinition(sourceFile, typeAlias) // Check for enum const enumDef = sourceFile.getEnum(typeName) if (enumDef) return extractTypeDefinition(sourceFile, enumDef) return null } function getTypeDefinition(typeName, packageName, sourceFilePath) { if (!typeName || PRIMITIVE_TYPES.has(typeName)) { return null } // Handle Promise and array types - extract base type let baseTypeName = typeName if (baseTypeName.startsWith("Promise<")) { const match = baseTypeName.match(/Promise<(.+)>/) if (match) baseTypeName = match[1] } if (baseTypeName.endsWith("[]")) { baseTypeName = baseTypeName.slice(0, -2) } // Check cache first const cacheKey = `${packageName}:${baseTypeName}` if (typeCache.has(cacheKey)) { return typeCache.get(cacheKey) } let definition = null try { // First check source file if provided if (sourceFilePath) { const fullSourcePath = path.resolve(process.cwd(), "../", sourceFilePath) if (fs.existsSync(fullSourcePath)) { const project = new Project({skipAddingFilesFromTsConfig: true}) const sourceFile = project.addSourceFileAtPath(fullSourcePath) definition = findTypeInFile(sourceFile, baseTypeName) } } // If not found, search package src directory if (!definition) { const packageSrcDir = path.resolve( process.cwd(), "../", packageName, "src" ) if (fs.existsSync(packageSrcDir)) { const project = new Project({skipAddingFilesFromTsConfig: true}) project.addSourceFilesAtPaths(`${packageSrcDir}/**/*.ts`) for (const sourceFile of project.getSourceFiles()) { definition = findTypeInFile(sourceFile, baseTypeName) if (definition) break } } } // Cache the result typeCache.set(cacheKey, definition) return definition } catch (error) { console.warn( `Error getting type definition for ${typeName}: ${error.message}` ) return null } } function processTypeForDisplay( typeString, coreTypes, packageName, sourceFilePath ) { if (!typeString) { return { displayType: typeString, hasLink: false, linkedType: null, typeDefinition: null, } } const extractedType = extractTypeName(typeString) // Check if type exists in typedefs package if (isTypeInTypedefs(extractedType, coreTypes)) { let linkType = extractedType let linkFragment = extractedType.toLowerCase() // Handle Promise<Type> - link to the inner type if (extractedType.startsWith("Promise<") && extractedType.endsWith(">")) { const innerType = extractedType.slice(8, -1).trim() linkFragment = innerType.toLowerCase() } // Handle Array types if (extractedType.endsWith("[]")) { const baseType = extractedType.slice(0, -2).trim() linkFragment = baseType.toLowerCase() } return { displayType: extractedType, hasLink: true, linkedType: `[\`${extractedType}\`](../types#${linkFragment})`, typeDefinition: null, } } // Check if it's a non-primitive type that should have a definition shown if (isNonPrimitiveType(extractedType)) { const typeDefinition = getTypeDefinition( extractedType, packageName, sourceFilePath ) return { displayType: extractedType, hasLink: false, linkedType: null, typeDefinition: formatTypeScript(typeDefinition || extractedType), // Show the type itself if no definition found } } // For primitive types or simple types, just show the type name return { displayType: extractedType, hasLink: false, linkedType: null, typeDefinition: null, } } function processFunction(func, packageName, coreTypes) { // Escape MDX characters in description if (func.description) { func.description = escapeMDXCharacters(func.description) } // Process parameters func.parameters = func.parameters.map(param => { const typeInfo = processTypeForDisplay( param.type, coreTypes, packageName, func.sourceFilePath ) // Escape MDX characters in parameter description const description = param.description ? escapeMDXCharacters(param.description) : param.description return { ...param, type: typeInfo.displayType, description, linkedType: typeInfo.linkedType, hasLink: typeInfo.hasLink, typeDefinition: typeInfo.typeDefinition, } }) // Process return type const returnTypeInfo = processTypeForDisplay( func.returnType, coreTypes, packageName, func.sourceFilePath ) func.returnType = returnTypeInfo.displayType func.returnHasLink = returnTypeInfo.hasLink func.linkedType = returnTypeInfo.linkedType func.returnTypeDefinition = returnTypeInfo.typeDefinition return func } function truncateDescription(description, maxLength = 80) { if (!description || description.length <= maxLength) { // Still normalize whitespace even for short descriptions return description ? description.replace(/\s+/g, " ").trim() : description } // Remove line breaks and normalize whitespace const normalizedDescription = description.replace(/\s+/g, " ").trim() // Find the last space before the maxLength to avoid cutting words let truncateAt = maxLength const lastSpace = normalizedDescription.lastIndexOf(" ", maxLength) if (lastSpace > maxLength * 0.8) { // Only use space if it's not too far back truncateAt = lastSpace } return normalizedDescription.substring(0, truncateAt).trim() + "..." } function generateNamespacePage(templates, outputDir, packageName, namespace) { const coreTypes = extractCoreTypes() // Deduplicate functions with the same name const uniqueFunctions = [] const seenFunctionNames = new Set() namespace.functions.forEach(func => { if (!seenFunctionNames.has(func.name)) { seenFunctionNames.add(func.name) uniqueFunctions.push(func) } }) // Sort functions alphabetically, case insensitively uniqueFunctions.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()) ) // Process each function to add type definitions and links const processedFunctions = uniqueFunctions.map(func => { const processedFunc = processFunction(func, packageName, coreTypes) // Add lowercase_name property for use in templates processedFunc.lowercase_name = func.name.charAt(0).toLowerCase() + func.name.slice(1) return processedFunc }) // Generate lowercase filename like functions const filename = namespace.name.charAt(0).toLowerCase() + namespace.name.slice(1) generatePage(templates, "namespace", path.join(outputDir, `${filename}.md`), { packageName, packageFirstWord: getFirstWord(packageName), namespaceName: namespace.name, namespaceDescription: namespace.description, functions: processedFunctions, }) } module.exports = {generateNamespacePage} ================================================ FILE: docs-generator/generators/generate-package-page.js ================================================ const path = require("path") const {generatePage, parseConfigCustomData, getFirstWord} = require("./utils") const fs = require("fs") function truncateDescription(description, maxLength = 80) { if (!description || description.length <= maxLength) { // Still normalize whitespace even for short descriptions return description ? description.replace(/\s+/g, " ").trim() : description } // Remove line breaks and normalize whitespace const normalizedDescription = description.replace(/\s+/g, " ").trim() // Find the last space before the maxLength to avoid cutting words let truncateAt = maxLength const lastSpace = normalizedDescription.lastIndexOf(" ", maxLength) if (lastSpace > maxLength * 0.8) { // Only use space if it's not too far back truncateAt = lastSpace } return normalizedDescription.substring(0, truncateAt).trim() + "..." } function getPackageDescription(packageName) { try { const packageJsonPath = path.resolve(process.cwd(), "package.json") if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) return packageJson.description || "" } } catch (error) { console.warn( `Error reading package.json for ${packageName}: ${error.message}` ) } return "" } function generatePackagePage( templates, outputDir, packageName, functions, namespaces = [], allNamespaceFunctions = [] ) { const configPath = path.resolve(process.cwd(), "docs-generator.config.js") const {displayName, sections, extra} = parseConfigCustomData(configPath) const packageDescription = getPackageDescription(packageName) // Combine regular functions with namespace functions for the API reference const allFunctions = [...functions, ...allNamespaceFunctions] // Deduplicate functions with the same name const uniqueFunctions = [] const seenFunctionNames = new Set() allFunctions.forEach(func => { if (!seenFunctionNames.has(func.name)) { seenFunctionNames.add(func.name) uniqueFunctions.push(func) } }) // Process namespaces for display const processedNamespaces = namespaces.map(namespace => ({ ...namespace, displayDescription: truncateDescription(namespace.description), // Only truncate for display type: "namespace", // Mark as namespace for sorting isNamespace: true, // Add boolean flag for template displayName: namespace.name, filePath: `./${namespace.name.charAt(0).toLowerCase() + namespace.name.slice(1)}.md`, })) // Process functions for display const processedFunctions = uniqueFunctions.map(func => ({ ...func, lowercase_name: func.name.charAt(0).toLowerCase() + func.name.slice(1), displayDescription: truncateDescription(func.description), // Only truncate for display type: "function", // Mark as function for sorting isNamespace: false, // Add boolean flag for template displayName: func.namespace ? `${func.namespace}.${func.name}` : func.name, filePath: func.namespace ? `./${func.namespace.charAt(0).toLowerCase() + func.namespace.slice(1)}.md#${func.name}` : `./${func.name.charAt(0).toLowerCase() + func.name.slice(1)}.md`, })) // Combine functions and namespaces for unified sorting const allApiItems = [...processedFunctions, ...processedNamespaces] // Sort all items alphabetically, case insensitively allApiItems.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()) ) generatePage(templates, "package", path.join(outputDir, "index.md"), { packageName, packageFirstWord: getFirstWord(packageName), displayName: displayName || `@onflow/${packageName}`, displayDescription: packageDescription || `${packageName} package documentation.`, customOverview: sections.overview, customRequirements: sections.requirements, customImporting: sections.importing, extra, functions: uniqueFunctions, // Keep original functions with full descriptions for individual pages namespaces: processedNamespaces, allApiItems, // Use processed items with truncated descriptions for the index }) } module.exports = {generatePackagePage} ================================================ FILE: docs-generator/generators/generate-root-page.js ================================================ const path = require("path") const fs = require("fs") const {generatePage, parseConfigCustomData} = require("./utils") function getPackageDescription(packageName) { try { const packageJsonPath = path.resolve( process.cwd(), "..", packageName, "package.json" ) if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) return packageJson.description || "" } } catch (error) { console.warn( `Error reading package.json for ${packageName}: ${error.message}` ) } return "" } function generateRootPage(templates, packagesDir, currentPackageName) { const configPath = path.resolve(process.cwd(), "docs-generator.config.js") const {displayName} = parseConfigCustomData(configPath) const currentDisplayName = displayName || `@onflow/${currentPackageName}` const currentPackageDescription = getPackageDescription(currentPackageName) || `${currentPackageName} package documentation.` const rootPagePath = path.join(packagesDir, "index.md") const packages = [] // Read existing packages from the file if it exists if (fs.existsSync(rootPagePath)) { try { const content = fs.readFileSync(rootPagePath, "utf8") const regex = /\- \[(.*?)\]\(\.\/(.*?)\/index\.md\)/g let match while ((match = regex.exec(content)) !== null) { if (match[1] && match[2] && match[1] !== "Type Definitions") { packages.push({ displayName: match[1], packageName: match[2], displayDescription: getPackageDescription(match[2]), }) } } } catch (error) { console.warn(`Error reading root page: ${error.message}`) } } // Check if current package already exists const packageExists = packages.some( pkg => pkg.packageName === currentPackageName ) // Add it if not already in the list if (!packageExists) { packages.push({ displayName: currentDisplayName, packageName: currentPackageName, displayDescription: currentPackageDescription, }) } // Sort packages by display name packages.sort((a, b) => a.displayName.localeCompare(b.displayName)) // Generate the root page generatePage(templates, "root", rootPagePath, {packages}) } module.exports = {generateRootPage} ================================================ FILE: docs-generator/generators/generate-types-page.js ================================================ const path = require("path") const {Project, Node, TypeFormatFlags} = require("ts-morph") const {generatePage} = require("./utils") function decodeHtmlEntities(text) { if (!text) return text const decoded = text .replace(/</g, "<") .replace(/>/g, ">") .replace(/&/g, "&") .replace(/"/g, '"') .replace(/'/g, "'") // Escape also pipe characters which would break markdown tables return decoded.replace(/\|/g, "\\|") } function extractJSDocDescription(node) { if (!node) return null try { if (typeof node.getJsDocs === "function") { const jsDocs = node.getJsDocs() if (jsDocs && jsDocs.length > 0) { const jsDoc = jsDocs[0] const description = jsDoc.getDescription() || "" return description.trim() || null } } // Fallback: try to parse JSDoc from the node leading comments if (typeof node.getLeadingCommentRanges === "function") { const commentRanges = node.getLeadingCommentRanges() if (commentRanges && commentRanges.length > 0) { const commentText = commentRanges .map(range => range.getText()) .join("\n") // Simple regex to extract JSDoc description const match = /\/\*\*\s*([\s\S]*?)\s*\*\//.exec(commentText) if (match && match[1]) { const description = match[1].replace(/^\s*\*\s?/gm, "").trim() return description || null } } } } catch (error) { console.warn(`Error extracting JSDoc description: ${error.message}`) } return null } function extractInterfaces(sourceFiles) { const interfaces = [] sourceFiles.forEach(sourceFile => { // Get exported interfaces sourceFile.getInterfaces().forEach(iface => { if (!iface.isExported()) return const name = iface.getName() const description = extractJSDocDescription(iface) // Extract properties const properties = iface.getProperties().map(prop => { const propName = prop.getName() const propType = decodeHtmlEntities( prop.getType().getText(undefined, TypeFormatFlags.None) ) const propDescription = extractJSDocDescription(prop) return { name: propName, type: propType, description: propDescription, } }) interfaces.push({ name, description, properties, importStatement: `import { type ${name} } from "@onflow/fcl"`, }) }) }) // Sort interfaces alphabetically by name return interfaces.sort((a, b) => a.name.localeCompare(b.name)) } function extractTypeAliases(sourceFiles) { const types = [] sourceFiles.forEach(sourceFile => { // Get exported type aliases sourceFile.getTypeAliases().forEach(typeAlias => { if (!typeAlias.isExported()) return const name = typeAlias.getName() const description = extractJSDocDescription(typeAlias) const type = decodeHtmlEntities( typeAlias.getType().getText(undefined, TypeFormatFlags.None) ) // For object types, try to extract properties const properties = [] const aliasType = typeAlias.getType() if (aliasType.isObject()) { const propSymbols = aliasType.getProperties() propSymbols.forEach(propSymbol => { const propName = propSymbol.getName() let propType = "unknown" try { const valueDeclaration = propSymbol.getValueDeclaration() if (valueDeclaration) { if (Node.isPropertySignature(valueDeclaration)) { const typeNode = valueDeclaration.getTypeNode() if (typeNode) { propType = typeNode.getText() } } } // If we couldn't get the type from the declaration, use the symbol's type if (propType === "unknown") { propType = propSymbol .getTypeAtLocation(typeAlias) .getText(undefined, TypeFormatFlags.None) } properties.push({ name: propName, type: decodeHtmlEntities(propType), description: extractJSDocDescription(propSymbol.getDeclarations()[0]) || null, }) } catch (error) { console.warn( `Error extracting property ${propName} from type ${name}: ${error.message}` ) properties.push({ name: propName, type: "unknown", description: null, }) } }) } types.push({ name, description, type, properties, importStatement: `import { type ${name} } from "@onflow/fcl"`, }) }) }) // Sort type aliases alphabetically by name return types.sort((a, b) => a.name.localeCompare(b.name)) } function extractEnums(sourceFiles) { const enums = [] sourceFiles.forEach(sourceFile => { // Get exported enums sourceFile.getEnums().forEach(enumDef => { if (!enumDef.isExported()) return const name = enumDef.getName() const description = extractJSDocDescription(enumDef) // Extract members const members = enumDef.getMembers().map(member => { const memberName = member.getName() const memberDescription = extractJSDocDescription(member) let memberValue = null // Try to get the value const valueNode = member.getInitializer() if (valueNode) { memberValue = valueNode.getText() } return { name: memberName, value: memberValue, description: memberDescription, } }) enums.push({ name, description, members, importStatement: `import { ${name} } from "@onflow/fcl"`, }) }) }) // Sort enums alphabetically by name return enums.sort((a, b) => a.name.localeCompare(b.name)) } function generateTypesPage(templates, outputDir) { // Path to the typedefs package const typedefsDir = path.resolve(process.cwd(), "../typedefs") const typedefsSrcDir = path.join(typedefsDir, "src") // Initialize ts-morph project const project = new Project({ skipAddingFilesFromTsConfig: true, }) // Add source files from typedefs package project.addSourceFilesAtPaths(`${typedefsSrcDir}/**/*.ts`) const sourceFiles = project.getSourceFiles() // Extract types data const interfaces = extractInterfaces(sourceFiles) const types = extractTypeAliases(sourceFiles) const enums = extractEnums(sourceFiles) // Generate the types index page generatePage(templates, "types", path.join(outputDir, "index.md"), { interfaces, types, enums, }) } module.exports = {generateTypesPage} ================================================ FILE: docs-generator/generators/index.js ================================================ const {generatePackagePage} = require("./generate-package-page") const {generateFunctionPage} = require("./generate-function-page") const {generateTypesPage} = require("./generate-types-page") const {generateRootPage} = require("./generate-root-page") const {generateNamespacePage} = require("./generate-namespace-page") module.exports = { generatePackagePage, generateFunctionPage, generateTypesPage, generateRootPage, generateNamespacePage, } ================================================ FILE: docs-generator/generators/utils/extract-utils.js ================================================ function getFirstWord(packageName) { return packageName.split("-")[0] } module.exports = {getFirstWord} ================================================ FILE: docs-generator/generators/utils/generate-page.js ================================================ const fs = require("fs") function generatePage(templates, templateName, outputPath, context) { try { const content = templates[templateName](context) fs.writeFileSync(outputPath, content) } catch (error) { console.error(`Error generating ${templateName} page: ${error.message}`) throw error } } module.exports = {generatePage} ================================================ FILE: docs-generator/generators/utils/index.js ================================================ const {generatePage} = require("./generate-page") const {parseConfigCustomData} = require("./parse-config-custom-data") const {getFirstWord} = require("./extract-utils") module.exports = {generatePage, parseConfigCustomData, getFirstWord} ================================================ FILE: docs-generator/generators/utils/parse-config-custom-data.js ================================================ const fs = require("fs") function parseConfigCustomData(configPath) { // Parse config custom data if present and return custom one or null for default const config = fs.existsSync(configPath) ? require(configPath) : null return { displayName: config?.customData?.displayName || null, sections: { overview: config?.customData?.sections?.overview || null, requirements: config?.customData?.sections?.requirements || null, importing: config?.customData?.sections?.importing || null, }, extra: config?.customData?.extra || null, } } module.exports = {parseConfigCustomData} ================================================ FILE: docs-generator/templates/function.hbs ================================================ --- title: "{{name}}" description: "{{name}} function documentation." --- <!-- THIS DOCUMENT IS AUTO-GENERATED FROM [onflow/{{packageName}}/{{sourceFilePath}}](https://github.com/onflow/fcl-js/tree/master/packages/{{packageName}}/{{sourceFilePath}}). DO NOT EDIT MANUALLY --> # {{name}} {{#if description}} {{{description}}} {{/if}} ## Import You can import the entire package and access the function: ```typescript import * as {{packageFirstWord}} from "@onflow/{{packageName}}" {{packageFirstWord}}.{{name}}({{#each parameters}}{{#unless @first}}, {{/unless}}{{name}}{{/each}}) ``` Or import directly the specific function: ```typescript import { {{name}} } from "@onflow/{{packageName}}" {{name}}({{#each parameters}}{{#unless @first}}, {{/unless}}{{name}}{{/each}}) ``` {{#if customExample}} ## Usage ```typescript {{{customExample}}} ``` {{/if}} {{#if parameters.length}} ## Parameters {{#each parameters}} ### `{{name}}` {{#unless required}}(optional){{/unless}} {{#if expandedType}} - Type: `{{{type}}}` #### Properties: {{#each expandedType}} - **`{{@key}}`** {{#if this.optional}}(optional){{/if}} - Type: `{{{this.type}}}`{{#if this.description}} - {{{this.description}}}{{/if}} {{/each}} {{else}} {{#if typeDefinition}}{{#unless hasLink}} - Type: ```typescript {{{typeDefinition}}} ``` {{else}} - Type: {{#if hasLink}}{{{linkedType}}}{{else}}`{{{type}}}`{{/if}} {{/unless}}{{else}} - Type: {{#if hasLink}}{{{linkedType}}}{{else}}`{{{type}}}`{{/if}} {{/if}} {{#if description}} - Description: {{{description}}} {{/if}} {{#if nestedParams}} #### Properties: {{#each nestedParams}} - **`{{name}}`** {{#unless required}}(optional){{/unless}} - {{{description}}} {{/each}} {{/if}} {{/if}} {{/each}} {{/if}} ## Returns {{#if returnTypeDefinition}} ```typescript {{{returnTypeDefinition}}} ``` {{else}} {{#if returnHasLink}}{{{linkedType}}}{{else}}`{{{returnType}}}`{{/if}} {{/if}} {{#if returnDescription}} {{{returnDescription}}} {{/if}} --- ================================================ FILE: docs-generator/templates/namespace.hbs ================================================ --- title: "{{namespaceName}}" description: "{{namespaceDescription}}" --- <!-- THIS DOCUMENT IS AUTO-GENERATED FROM [onflow/{{packageName}}](https://github.com/onflow/fcl-js/tree/master/packages/{{packageName}}). DO NOT EDIT MANUALLY --> # {{namespaceName}} ## Overview {{namespaceDescription}} ## Functions {{#each functions}} ### {{name}} {{#if description}} {{{description}}} {{/if}} #### Import You can import the entire package and access the function: ```typescript import * as {{../packageFirstWord}} from "@onflow/{{../packageName}}" {{../packageFirstWord}}.{{../namespaceName}}.{{name}}({{#each parameters}}{{#unless @first}}, {{/unless}}{{name}}{{/each}}) ``` Or import the namespace directly: ```typescript import { {{../namespaceName}} } from "@onflow/{{../packageName}}" {{../namespaceName}}.{{name}}({{#each parameters}}{{#unless @first}}, {{/unless}}{{name}}{{/each}}) ``` {{#if customExample}} #### Usage ```typescript {{{customExample}}} ``` {{/if}} {{#if parameters.length}} #### Parameters {{#each parameters}} ##### `{{name}}`{{#unless required}} (optional){{/unless}} {{#if typeDefinition}}{{#unless hasLink}} - Type: ```typescript {{{typeDefinition}}} ``` {{else}} - Type: {{#if hasLink}}{{{linkedType}}}{{else}}`{{{type}}}`{{/if}} {{/unless}}{{else}} - Type: {{#if hasLink}}{{{linkedType}}}{{else}}`{{{type}}}`{{/if}} {{/if}} {{#if description}} - Description: {{{description}}} {{/if}} {{/each}} {{/if}} #### Returns {{#if returnTypeDefinition}} ```typescript {{{returnTypeDefinition}}} ``` {{else}} {{#if returnHasLink}}{{{linkedType}}}{{else}}`{{{returnType}}}`{{/if}} {{/if}} {{/each}} --- ================================================ FILE: docs-generator/templates/package.hbs ================================================ --- title: "{{displayName}}" description: "{{#if displayDescription}}{{displayDescription}}{{else}}{{displayName}} package documentation.{{/if}}" --- <!-- THIS DOCUMENT IS AUTO-GENERATED FROM [onflow/{{packageName}}](https://github.com/onflow/fcl-js/tree/master/packages/{{packageName}}). DO NOT EDIT MANUALLY --> # {{displayName}} ## Overview {{#if customOverview}} {{{customOverview}}} {{else}} The Flow {{packageName}} library provides a set of tools for developers to build applications on the Flow blockchain. {{/if}} ## Installation You can install the @onflow/{{packageName}} package using npm or yarn: ```bash npm install @onflow/{{packageName}} ``` Or using yarn: ```bash yarn add @onflow/{{packageName}} ``` ### Requirements {{#if customRequirements}} {{{customRequirements}}} {{else}} - Node.js 14.x or later {{/if}} ### Importing {{#if customImporting}} {{{customImporting}}} {{else}} You can import the entire package: ```typescript import * as {{packageFirstWord}} from "@onflow/{{packageName}}" ``` Or import specific functions: ```typescript import { functionName } from "@onflow/{{packageName}}" ``` {{/if}} {{#if extra}} {{{extra}}} {{/if}} ## API Reference This section contains documentation for all of the functions and namespaces in the {{packageName}} package. {{#each allApiItems}} {{#if isNamespace}} - [{{displayName}}]({{filePath}}) (namespace){{#if displayDescription}} - {{displayDescription}}{{/if}} {{else}} - [{{displayName}}]({{filePath}}){{#if displayDescription}} - {{displayDescription}}{{/if}} {{/if}} {{/each}} --- ================================================ FILE: docs-generator/templates/root.hbs ================================================ --- sidebar_position: 1 title: Packages Docs description: Packages documentation. --- <!-- THIS DOCUMENT IS AUTO-GENERATED FROM [onflow/fcl-js](https://github.com/onflow/fcl-js). DO NOT EDIT MANUALLY --> # Packages Docs A list of all packages available inside Flow Client Library (FCL) with functions and type definitions. {{#each packages}} - [{{this.displayName}}](./{{this.packageName}}/index.md) - {{this.displayDescription}} {{/each}} - [Type Definitions](./types/index.md) - Type definitions for the Flow Client Library (FCL) packages. --- ================================================ FILE: docs-generator/templates/types.hbs ================================================ --- title: Type Definitions description: Type definitions for the Flow Client Library (FCL) packages. --- <!-- THIS DOCUMENT IS AUTO-GENERATED FROM [onflow/typedefs](https://github.com/onflow/fcl-js/tree/master/packages/typedefs). DO NOT EDIT MANUALLY --> # Type Definitions Documentation for core types used throughout the Flow Client Library (FCL). {{#if interfaces.length}} ## Interfaces {{#each interfaces}} ### {{name}} ```typescript {{{importStatement}}} ``` {{#if description}} {{{description}}} {{/if}} {{#if properties.length}} **Properties:** | Name | Type | Description | | ---- | ---- | ----------- | {{#each properties}} | `{{name}}` | `{{{type}}}` | {{#if description}}{{{description}}}{{/if}} | {{/each}} {{/if}} {{/each}} {{/if}} {{#if types.length}} ## Types {{#each types}} ### {{name}} ```typescript {{{importStatement}}} ``` {{#if description}} {{{description}}} {{/if}} {{#if properties.length}} **Properties:** | Name | Type | Description | | ---- | ---- | ----------- | {{#each properties}} | `{{name}}` | `{{{type}}}` | {{#if description}}{{{description}}}{{/if}} | {{/each}} {{/if}} {{/each}} {{/if}} {{#if enums.length}} ## Enums {{#each enums}} ### {{name}} ```typescript {{{importStatement}}} ``` {{#if description}} {{{description}}} {{/if}} **Members:** | Name | Value | | ---- | ----- | {{#each members}} | `{{name}}` | {{#if value}}{{{value}}}{{else}}`"{{name}}"`{{/if}} | {{/each}} {{/each}} {{/if}} --- ================================================ FILE: docs-generator/utils/export-extractor.js ================================================ const path = require("path") const {Node} = require("ts-morph") const {parseJsDoc} = require("./jsdoc-parser") const { extractFunctionInfo, resolveReExportedFunction, } = require("./function-extractor") const {extractNamespaceFunctions} = require("./namespace-utils") function extractExportsFromEntryFile(sourceFile) { const functions = [] const namespaces = [] const processedReExports = new Set() // Track already processed re-exports const filePath = sourceFile.getFilePath() const relativeFilePath = path.relative(process.cwd(), filePath) try { // Get all import declarations to track namespaces const importDeclarations = sourceFile.getImportDeclarations() const actualNamespaceImports = new Map() // Only for "import * as X" style imports const namedImports = new Map() // For regular named imports like "import {build}" const typeOnlyImports = new Set() // Track type-only imports importDeclarations.forEach(importDecl => { // Skip entire type-only import declarations if (importDecl.isTypeOnly && importDecl.isTypeOnly()) { return } // Handle regular named imports const namedImportsList = importDecl.getNamedImports() namedImportsList.forEach(namedImport => { const name = namedImport.getName() // Check if this is a type-only import (either the import declaration or the named import) if ( namedImport.isTypeOnly() || (importDecl.isTypeOnly && importDecl.isTypeOnly()) ) { typeOnlyImports.add(name) return // Skip adding to namedImports } namedImports.set(name, importDecl) }) // Handle actual namespace imports like "import * as types" const namespaceImport = importDecl.getNamespaceImport() if (namespaceImport) { const name = namespaceImport.getText() // Only add if it's not a type-only import if (!(importDecl.isTypeOnly && importDecl.isTypeOnly())) { actualNamespaceImports.set(name, importDecl) } } }) // Get all export declarations to find re-exports and namespace exports with aliases sourceFile.getExportDeclarations().forEach(exportDecl => { // Skip type-only exports if (exportDecl.isTypeOnly()) { return } const namedExports = exportDecl.getNamedExports() namedExports.forEach(namedExport => { // Skip type-only named exports if (namedExport.isTypeOnly()) { return } const exportName = namedExport.getName() const alias = namedExport.getAliasNode()?.getText() const finalName = alias || exportName // For aliased exports, exportName is the original and alias is the final name // When resolving, we want to look up the original name (exportName) const originalName = exportName // Skip if this is a type-only import if (typeOnlyImports.has(exportName)) { return } // Check if this is a re-export from another module const moduleSpecifier = exportDecl.getModuleSpecifier() if (moduleSpecifier) { const moduleSpecifierValue = moduleSpecifier.getLiteralValue() const reExportedFuncInfo = resolveReExportedFunction( sourceFile, originalName, moduleSpecifierValue ) if (reExportedFuncInfo) { reExportedFuncInfo.name = finalName functions.push(reExportedFuncInfo) // Track that this function was processed as a re-export processedReExports.add(finalName) } } else { // This is an export of something imported - check if it's an actual namespace if ( actualNamespaceImports.has(exportName) && !typeOnlyImports.has(exportName) ) { const importDecl = actualNamespaceImports.get(exportName) const namespaceFunctions = extractNamespaceFunctions( sourceFile, exportName, importDecl ) // Only add as namespace if it actually has functions if (namespaceFunctions.length > 0) { namespaces.push({ name: finalName, functions: namespaceFunctions, description: `Namespace containing ${finalName} utilities`, }) } } // Also check if it's a named import that might be a namespace object else if ( namedImports.has(exportName) && !typeOnlyImports.has(exportName) ) { const importDecl = namedImports.get(exportName) const namespaceFunctions = extractNamespaceFunctions( sourceFile, exportName, importDecl ) // Only add as namespace if it actually has functions if (namespaceFunctions.length > 0) { namespaces.push({ name: finalName, functions: namespaceFunctions, description: `Namespace containing ${finalName} utilities`, }) } } } }) }) // Get exported declarations from the current file sourceFile.getExportedDeclarations().forEach((declarations, name) => { // Skip if this function was already processed as a re-export if (processedReExports.has(name)) { return } declarations.forEach(declaration => { // Skip type declarations, interfaces, and type aliases if ( Node.isTypeAliasDeclaration(declaration) || Node.isInterfaceDeclaration(declaration) || Node.isEnumDeclaration(declaration) ) { // Skip these as they are types, not functions return } const funcInfo = extractFunctionInfo(declaration, name, sourceFile) if (funcInfo) { functions.push(funcInfo) } // Check if this is a namespace export for variable declarations else if (Node.isVariableDeclaration(declaration)) { // Only check if this is a namespace export if it's an actual namespace import // and it's not a type-only import if ( actualNamespaceImports.has(name) && !typeOnlyImports.has(name) && !namespaces.some(ns => ns.name === name) ) { const importDecl = actualNamespaceImports.get(name) const namespaceFunctions = extractNamespaceFunctions( sourceFile, name, importDecl ) // Only add as namespace if it actually has functions if (namespaceFunctions.length > 0) { const jsDocInfo = parseJsDoc(declaration) namespaces.push({ name, functions: namespaceFunctions, description: jsDocInfo.description || `Namespace containing ${name} utilities`, }) } } // Also check if it's a named import that might be a namespace object else if ( namedImports.has(name) && !typeOnlyImports.has(name) && !namespaces.some(ns => ns.name === name) ) { const importDecl = namedImports.get(name) const namespaceFunctions = extractNamespaceFunctions( sourceFile, name, importDecl ) // Only add as namespace if it actually has functions if (namespaceFunctions.length > 0) { const jsDocInfo = parseJsDoc(declaration) namespaces.push({ name, functions: namespaceFunctions, description: jsDocInfo.description || `Namespace containing ${name} utilities`, }) } } } }) }) } catch (e) { console.warn(`Error extracting exports from entry file: ${e.message}`) console.warn(e.stack) } return {functions, namespaces} } module.exports = { extractExportsFromEntryFile, } ================================================ FILE: docs-generator/utils/file-utils.js ================================================ const fs = require("fs") const path = require("path") function discoverWorkspacePackages() { try { // Get the workspace root (2 levels up from current package directory) const workspaceRoot = path.resolve(process.cwd(), "../..") const packagesDir = path.join(workspaceRoot, "packages") if (!fs.existsSync(packagesDir)) { console.warn("Packages directory not found, using current package only") return [] } const packagePaths = [] const packageDirs = fs .readdirSync(packagesDir, {withFileTypes: true}) .filter(dirent => dirent.isDirectory()) .map(dirent => dirent.name) for (const packageDir of packageDirs) { const srcPath = path.join(packagesDir, packageDir, "src") if (fs.existsSync(srcPath)) { packagePaths.push(`${srcPath}/**/*.ts`) } } return packagePaths } catch (e) { console.warn(`Error discovering workspace packages: ${e.message}`) return [] } } module.exports = { discoverWorkspacePackages, } ================================================ FILE: docs-generator/utils/function-extractor.js ================================================ const path = require("path") const fs = require("fs") const {Node} = require("ts-morph") const {parseJsDoc} = require("./jsdoc-parser") const { cleanupTypeText, toCamelCase, escapeParameterNameForMDX, escapeTextForMDX, } = require("./type-utils") function extractFunctionInfo( declaration, functionName, sourceFile, namespace = null ) { try { let funcInfo = null // Handle function declarations if (Node.isFunctionDeclaration(declaration)) { const jsDocInfo = parseJsDoc(declaration) const parameters = declaration.getParameters().map(param => { let paramName = param.getName() const paramType = cleanupTypeText(param.getType().getText()) const paramJsDoc = jsDocInfo.params && jsDocInfo.params[paramName] // Handle destructured parameters by using camelCase of the type name if (paramName.includes("{") && paramName.includes("}")) { // Extract the type name from the parameter type // Handle both direct types (AccountProofData) and import types (import("...").AccountProofData) const typeText = param.getType().getText() let typeMatch = typeText.match(/^([A-Z][a-zA-Z0-9]*)/) // Direct type if (!typeMatch) { typeMatch = typeText.match(/import\([^)]+\)\.([A-Z][a-zA-Z0-9]*)/) // Import type } if (typeMatch && typeMatch[1]) { paramName = toCamelCase(typeMatch[1]) } else { // Fallback to "options" if we can't extract a type name paramName = "options" } } // Handle nested JSDoc parameters (e.g., queryOptions.height) let nestedParams = [] if (jsDocInfo.params) { Object.keys(jsDocInfo.params).forEach(jsDocParamName => { if (jsDocParamName.startsWith(paramName + ".")) { const nestedParamName = jsDocParamName.substring( paramName.length + 1 ) // Try to extract type information from the parameter's TypeScript type let nestedParamType = "any" try { const paramTypeSymbol = param.getType().getSymbol() if (paramTypeSymbol) { const typeDeclaration = paramTypeSymbol.getDeclarations()?.[0] if ( typeDeclaration && Node.isInterfaceDeclaration(typeDeclaration) ) { // Find the property in the interface const property = typeDeclaration.getProperty(nestedParamName) if (property) { const propertyType = property.getType() nestedParamType = cleanupTypeText(propertyType.getText()) } } } } catch (e) { // Fallback to any if type extraction fails nestedParamType = "any" } nestedParams.push({ name: escapeParameterNameForMDX(nestedParamName), type: nestedParamType, required: true, // Default to required description: escapeTextForMDX(jsDocInfo.params[jsDocParamName]) || "", }) } }) } return { name: escapeParameterNameForMDX(paramName), type: paramType, required: !param.isOptional(), description: escapeTextForMDX(paramJsDoc) || "", nestedParams: nestedParams.length > 0 ? nestedParams : undefined, } }) // Get the actual return type, not the function signature let returnType = "any" try { const funcReturnType = declaration.getReturnType() const returnTypeText = funcReturnType.getText() // If the return type text looks like a function signature, extract just the return part // But don't apply this logic to object literal types (which start with '{') if ( returnTypeText.includes("=>") && !returnTypeText.trim().startsWith("{") ) { const returnPart = returnTypeText.split("=>").pop()?.trim() if (returnPart) { returnType = cleanupTypeText(returnPart) } else { returnType = cleanupTypeText(returnTypeText) } } else { returnType = cleanupTypeText(returnTypeText) } } catch (e) { console.warn(`Error extracting return type: ${e.message}`) returnType = "any" } // Extract return description from JSDoc let returnDescription = null if (jsDocInfo.returns) { returnDescription = escapeTextForMDX(jsDocInfo.returns) } const filePath = sourceFile.getFilePath() const relativeFilePath = path.relative(process.cwd(), filePath) funcInfo = { name: functionName, returnType, returnDescription, parameters, description: jsDocInfo.description || "", customExample: jsDocInfo.example || "", sourceFilePath: relativeFilePath, } } // Handle variable declarations with function values else if (Node.isVariableDeclaration(declaration)) { let jsDocInfo = parseJsDoc(declaration) // If no JSDoc found on the declaration, try the parent VariableStatement if (!jsDocInfo.description) { const parentList = declaration.getParent() if (parentList) { const parentStatement = parentList.getParent() if (parentStatement) { jsDocInfo = parseJsDoc(parentStatement) } } } const initializer = declaration.getInitializer() // Check for function calls that might wrap a function (like withGlobalContext(createAccount)) if (initializer && Node.isCallExpression(initializer)) { const args = initializer.getArguments() // Check if the call expression itself is calling a function we can analyze const callExpression = initializer.getExpression() if (Node.isIdentifier(callExpression)) { const functionName_inner = callExpression.getText() // Look for the function being called (like createQuery) const calledFunction = sourceFile.getFunction(functionName_inner) if (calledFunction) { // Look for inner function declarations within the called function const innerFunctions = calledFunction.getFunctions() if (innerFunctions.length > 0) { // Get the first inner function (usually the one being returned) const innerFunction = innerFunctions[0] const innerFuncInfo = extractFunctionInfo( innerFunction, functionName, sourceFile, namespace ) if (innerFuncInfo) { // Merge JSDoc from the exported variable return { ...innerFuncInfo, description: jsDocInfo.description || innerFuncInfo.description, customExample: jsDocInfo.example || innerFuncInfo.customExample, } } } // Also look for inner variable declarations that might contain arrow functions const innerVariables = calledFunction.getVariableDeclarations() if (innerVariables.length > 0) { // Look for a variable with the same name as the function we're looking for const matchingVariable = innerVariables.find( v => v.getName() === functionName ) if (matchingVariable) { const innerFuncInfo = extractFunctionInfo( matchingVariable, functionName, sourceFile, namespace ) if (innerFuncInfo) { // Merge JSDoc from the exported variable return { ...innerFuncInfo, description: jsDocInfo.description || innerFuncInfo.description, customExample: jsDocInfo.example || innerFuncInfo.customExample, } } } } } } if (args.length > 0) { const firstArg = args[0] // If the first argument is an identifier, try to resolve it to a function if (Node.isIdentifier(firstArg)) { const argName = firstArg.getText() // Look for the function with this name in the source file const referencedFunction = sourceFile.getFunction(argName) if (referencedFunction) { // Look for inner function declarations within the referenced function const innerFunctions = referencedFunction.getFunctions() if (innerFunctions.length > 0) { // Get the first inner function (usually the one being returned) const innerFunction = innerFunctions[0] const innerFuncInfo = extractFunctionInfo( innerFunction, functionName, sourceFile, namespace ) if (innerFuncInfo) { // Merge JSDoc from the exported variable, including nested parameters const mergedParameters = innerFuncInfo.parameters.map( param => { // Check if there are nested JSDoc parameters for this parameter let nestedParams = [] if (jsDocInfo.params) { Object.keys(jsDocInfo.params).forEach( jsDocParamName => { if (jsDocParamName.startsWith(param.name + ".")) { const nestedParamName = jsDocParamName.substring( param.name.length + 1 ) // Extract nested parameter type from the parameter's TypeScript interface let nestedParamType = "any" try { // Get the parameter from the inner function for type extraction const innerParams = innerFunction.getParameters() const matchingParam = innerParams.find(p => { let pName = p.getName() if ( pName.includes("{") && pName.includes("}") ) { const typeText = p.getType().getText() let typeMatch = typeText.match(/^([A-Z][a-zA-Z0-9]*)/) if (!typeMatch) { typeMatch = typeText.match( /import\([^)]+\)\.([A-Z][a-zA-Z0-9]*)/ ) } if (typeMatch && typeMatch[1]) { pName = toCamelCase(typeMatch[1]) } } return pName === param.name }) if (matchingParam) { const paramTypeSymbol = matchingParam .getType() .getSymbol() if (paramTypeSymbol) { const typeDeclaration = paramTypeSymbol.getDeclarations()?.[0] if ( typeDeclaration && Node.isInterfaceDeclaration( typeDeclaration ) ) { const property = typeDeclaration.getProperty( nestedParamName ) if (property) { const propertyType = property.getType() nestedParamType = cleanupTypeText( propertyType.getText() ) } } } } } catch (e) { // Fallback to any nestedParamType = "any" } nestedParams.push({ name: escapeParameterNameForMDX( nestedParamName ), type: nestedParamType, required: true, // Default to required description: escapeTextForMDX( jsDocInfo.params[jsDocParamName] ) || "", }) } } ) } return { ...param, // Override description from JSDoc if available description: jsDocInfo.params && jsDocInfo.params[param.name] ? escapeTextForMDX(jsDocInfo.params[param.name]) : param.description, nestedParams: nestedParams.length > 0 ? nestedParams : undefined, } } ) return { ...innerFuncInfo, description: jsDocInfo.description || innerFuncInfo.description, customExample: jsDocInfo.example || innerFuncInfo.customExample, parameters: mergedParameters, } } } // Fallback: try to extract info directly from the referenced function const referencedFuncInfo = extractFunctionInfo( referencedFunction, functionName, sourceFile, namespace ) if (referencedFuncInfo) { // Override with JSDoc from the exported variable return { ...referencedFuncInfo, description: jsDocInfo.description || referencedFuncInfo.description, customExample: jsDocInfo.example || referencedFuncInfo.customExample, } } } } } } if ( initializer && (Node.isFunctionExpression(initializer) || Node.isArrowFunction(initializer)) ) { const parameters = initializer.getParameters().map(param => { let paramName = param.getName() const paramType = cleanupTypeText(param.getType().getText()) const paramJsDoc = jsDocInfo.params && jsDocInfo.params[paramName] // Handle destructured parameters by using camelCase of the type name if (paramName.includes("{") && paramName.includes("}")) { // Extract the type name from the parameter type // Handle both direct types (AccountProofData) and import types (import("...").AccountProofData) const typeText = param.getType().getText() let typeMatch = typeText.match(/^([A-Z][a-zA-Z0-9]*)/) // Direct type if (!typeMatch) { typeMatch = typeText.match(/import\([^)]+\)\.([A-Z][a-zA-Z0-9]*)/) // Import type } if (typeMatch && typeMatch[1]) { paramName = toCamelCase(typeMatch[1]) } else { // Fallback to "options" if we can't extract a type name paramName = "options" } } // Handle nested JSDoc parameters (e.g., queryOptions.height) let nestedParams = [] if (jsDocInfo.params) { Object.keys(jsDocInfo.params).forEach(jsDocParamName => { if (jsDocParamName.startsWith(paramName + ".")) { const nestedParamName = jsDocParamName.substring( paramName.length + 1 ) // Try to extract type information from the parameter's TypeScript type let nestedParamType = "any" try { const paramTypeSymbol = param.getType().getSymbol() if (paramTypeSymbol) { const typeDeclaration = paramTypeSymbol.getDeclarations()?.[0] if ( typeDeclaration && Node.isInterfaceDeclaration(typeDeclaration) ) { // Find the property in the interface const property = typeDeclaration.getProperty(nestedParamName) if (property) { const propertyType = property.getType() nestedParamType = cleanupTypeText( propertyType.getText() ) } } } } catch (e) { // Fallback to any if type extraction fails nestedParamType = "any" } nestedParams.push({ name: escapeParameterNameForMDX(nestedParamName), type: nestedParamType, required: true, // Default to required description: escapeTextForMDX(jsDocInfo.params[jsDocParamName]) || "", }) } }) } return { name: escapeParameterNameForMDX(paramName), type: paramType, required: !param.isOptional(), description: escapeTextForMDX(paramJsDoc) || "", nestedParams: nestedParams.length > 0 ? nestedParams : undefined, } }) // Get the actual return type, not the function signature let returnType = "any" try { const funcReturnType = initializer.getReturnType() const returnTypeText = funcReturnType.getText() // If the return type text looks like a function signature, extract just the return part if (returnTypeText.includes("=>")) { const returnPart = returnTypeText.split("=>").pop()?.trim() if (returnPart) { returnType = cleanupTypeText(returnPart) } else { returnType = cleanupTypeText(returnTypeText) } } else { returnType = cleanupTypeText(returnTypeText) } } catch (e) { console.warn(`Error extracting return type: ${e.message}`) returnType = "any" } // Extract return description from JSDoc let returnDescription = null if (jsDocInfo.returns) { returnDescription = escapeTextForMDX(jsDocInfo.returns) } const filePath = sourceFile.getFilePath() const relativeFilePath = path.relative(process.cwd(), filePath) funcInfo = { name: functionName, returnType, returnDescription, parameters, description: jsDocInfo.description || "", customExample: jsDocInfo.example || "", sourceFilePath: relativeFilePath, } } // Handle variable declarations with JSDoc that represent functions // (like resolve = pipe([...]) or other function-returning expressions) else if (jsDocInfo.description || jsDocInfo.params || jsDocInfo.returns) { // Extract parameter information from JSDoc if available const parameters = [] if (jsDocInfo.params) { // First, get all top-level parameters (those without dots) const topLevelParams = Object.keys(jsDocInfo.params).filter( paramName => !paramName.includes(".") ) topLevelParams.forEach(paramName => { const paramDesc = jsDocInfo.params[paramName] // Handle nested JSDoc parameters (e.g., queryOptions.height) let nestedParams = [] Object.keys(jsDocInfo.params).forEach(jsDocParamName => { if (jsDocParamName.startsWith(paramName + ".")) { const nestedParamName = jsDocParamName.substring( paramName.length + 1 ) // For JSDoc-only cases, try to infer type from variable declaration let nestedParamType = "any" try { const variableType = declaration.getType() const typeSymbol = variableType.getSymbol() if (typeSymbol) { const typeDeclaration = typeSymbol.getDeclarations()?.[0] if ( typeDeclaration && Node.isInterfaceDeclaration(typeDeclaration) ) { const property = typeDeclaration.getProperty(nestedParamName) if (property) { const propertyType = property.getType() nestedParamType = cleanupTypeText( propertyType.getText() ) } } } } catch (e) { // Fallback to any nestedParamType = "any" } nestedParams.push({ name: escapeParameterNameForMDX(nestedParamName), type: nestedParamType, required: true, // Default to required description: escapeTextForMDX(jsDocInfo.params[jsDocParamName]) || "", }) } }) parameters.push({ name: escapeParameterNameForMDX(paramName), type: "any", // Default type since we can't infer from call expressions required: true, // Default to required description: escapeTextForMDX(paramDesc) || "", nestedParams: nestedParams.length > 0 ? nestedParams : undefined, }) }) } // Get return type from JSDoc or try to infer from the variable type let returnType = "any" let returnDescription = null if (jsDocInfo.returns) { returnDescription = escapeTextForMDX(jsDocInfo.returns) } // Always try to get the actual TypeScript return type try { returnType = cleanupTypeText(declaration.getType().getText()) } catch (e) { // Fallback to any if type inference fails returnType = "any" } const filePath = sourceFile.getFilePath() const relativeFilePath = path.relative(process.cwd(), filePath) funcInfo = { name: functionName, returnType, returnDescription, parameters, description: jsDocInfo.description || "", customExample: jsDocInfo.example || "", sourceFilePath: relativeFilePath, } } } // Add namespace if provided if (funcInfo && namespace) { funcInfo.namespace = namespace } return funcInfo } catch (e) { console.warn( `Error extracting function info for ${functionName}: ${e.message}` ) return null } } function findFunctionInSourceFile(sourceFile, functionName) { try { // First, check if this function is re-exported from another module // If it is, we should NOT try to extract it directly here, but let the re-export resolution handle it const exportDeclarations = sourceFile.getExportDeclarations() for (const exportDecl of exportDeclarations) { const namedExports = exportDecl.getNamedExports() const hasExport = namedExports.some( namedExport => namedExport.getName() === functionName ) if (hasExport && exportDecl.getModuleSpecifier()) { return null // This will force the caller to use re-export resolution } } const exportedDeclarations = sourceFile.getExportedDeclarations() if (exportedDeclarations.has(functionName)) { const declarations = exportedDeclarations.get(functionName) for (const declaration of declarations) { // Skip export declarations - we want actual function implementations if (Node.isExportDeclaration(declaration)) { continue } const funcInfo = extractFunctionInfo( declaration, functionName, sourceFile ) if (funcInfo) { return funcInfo } } } return null } catch (e) { console.warn( `Error finding function ${functionName} in source file: ${e.message}` ) return null } } function resolveOnFlowPackage(packageName) { try { // Look for the package in the workspace packages directory // We need to account for the fact that the current working directory is already in packages/ const packagesDir = path.resolve(process.cwd(), "..") const packageDir = path.join(packagesDir, packageName) const packageJsonPath = path.join(packageDir, "package.json") if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) const entryFile = packageJson.source || packageJson.main || "src/index.ts" const entryFilePath = path.resolve(packageDir, entryFile) if (fs.existsSync(entryFilePath)) { return entryFilePath } } return null } catch (e) { console.warn(`Error resolving @onflow/${packageName}: ${e.message}`) return null } } function resolveReExportedFunction(sourceFile, exportName, moduleSpecifier) { try { let referencedSourceFile = null // Handle @onflow/ package specifiers if (moduleSpecifier.startsWith("@onflow/")) { const packageName = moduleSpecifier.replace("@onflow/", "") const packageEntryPath = resolveOnFlowPackage(packageName) if (packageEntryPath) { // Get the ts-morph project from the source file const project = sourceFile.getProject() // Try to get the source file, or add it if not already added referencedSourceFile = project.getSourceFile(packageEntryPath) if (!referencedSourceFile) { try { referencedSourceFile = project.addSourceFileAtPath(packageEntryPath) } catch (e) { console.warn( `Could not add source file at ${packageEntryPath}: ${e.message}` ) } } } } else { // Handle relative imports - existing logic const referencedSourceFiles = sourceFile.getReferencedSourceFiles() // Find the source file that matches the module specifier // Try exact matches first, then partial matches const candidates = [] for (const sf of referencedSourceFiles) { const fileName = path.basename(sf.getFilePath(), ".ts") const moduleFileName = path.basename(moduleSpecifier, ".ts") // Exact match (highest priority) if (fileName === moduleFileName) { candidates.push({sf, priority: 1}) } // Path includes the module specifier else if ( sf.getFilePath().includes(moduleSpecifier) || sf.getFilePath().includes(moduleSpecifier.replace("./", "")) ) { candidates.push({sf, priority: 2}) } } // Sort by priority and take the first (exact matches first) if (candidates.length > 0) { candidates.sort((a, b) => a.priority - b.priority) referencedSourceFile = candidates[0].sf } } if (referencedSourceFile) { const funcInfo = findFunctionInSourceFile( referencedSourceFile, exportName ) if (funcInfo) { return funcInfo } // If not found in the entry file, check if it's re-exported from elsewhere // Look through export declarations to find where this function comes from const exportDeclarations = referencedSourceFile.getExportDeclarations() for (const exportDecl of exportDeclarations) { const namedExports = exportDecl.getNamedExports() // Check if this export contains our function (including checking both name and alias) const hasExport = namedExports.some(namedExport => { const name = namedExport.getName() const alias = namedExport.getAliasNode()?.getText() const finalName = alias || name return finalName === exportName || name === exportName }) if (hasExport) { const moduleSpec = exportDecl.getModuleSpecifier() if (moduleSpec) { const moduleSpecValue = moduleSpec.getLiteralValue() // Find the original name to look up in the target module const matchingExport = namedExports.find(namedExport => { const name = namedExport.getName() const alias = namedExport.getAliasNode()?.getText() const finalName = alias || name return finalName === exportName || name === exportName }) const originalNameToLookup = matchingExport?.getName() || exportName // Recursively resolve from the module this export comes from return resolveReExportedFunction( referencedSourceFile, originalNameToLookup, moduleSpecValue ) } } } } return null } catch (e) { console.warn( `Error resolving re-exported function ${exportName} from ${moduleSpecifier}: ${e.message}` ) return null } } module.exports = { extractFunctionInfo, findFunctionInSourceFile, resolveReExportedFunction, } ================================================ FILE: docs-generator/utils/index.js ================================================ const {parseJsDoc} = require("./jsdoc-parser") const {cleanupTypeText, escapeParameterNameForMDX} = require("./type-utils") const { extractFunctionInfo, findFunctionInSourceFile, resolveReExportedFunction, } = require("./function-extractor") const {extractNamespaceFunctions} = require("./namespace-utils") const {discoverWorkspacePackages} = require("./file-utils") const {extractExportsFromEntryFile} = require("./export-extractor") module.exports = { parseJsDoc, cleanupTypeText, escapeParameterNameForMDX, extractFunctionInfo, findFunctionInSourceFile, resolveReExportedFunction, extractNamespaceFunctions, discoverWorkspacePackages, extractExportsFromEntryFile, } ================================================ FILE: docs-generator/utils/jsdoc-parser.js ================================================ function parseJsDoc(node) { try { // Try to get JSDoc using the standard API if (typeof node.getJsDocs === "function") { const jsDocs = node.getJsDocs() if (jsDocs && jsDocs.length > 0) { const jsDoc = jsDocs[0] // Parse tags if available let parsedTags = {} let description = "" if (typeof jsDoc.getTags === "function") { const tags = jsDoc.getTags() tags.forEach(tag => { const tagName = tag.getTagName() let comment = "" // Try to get comment text if (typeof tag.getComment === "function") { comment = tag.getComment() || "" } // Parse different tag types if (tagName === "description") { description = comment } // Parse param tags else if (tagName === "param") { if (!parsedTags.params) parsedTags.params = {} // Try to get parameter name from the tag let paramName = "" if (typeof tag.getName === "function") { paramName = tag.getName() } // If no name found, try to extract from comment if (!paramName && comment) { const paramMatch = comment.match(/^(\w+[\.\w]*)\s+(.*)$/) if (paramMatch) { paramName = paramMatch[1] comment = paramMatch[2] } } if (paramName) { parsedTags.params[paramName] = comment } } // Parse return tag else if (tagName === "returns" || tagName === "return") { // Handle multiple @returns tags by concatenating them if (parsedTags.returns) { parsedTags.returns += `\n• ${comment}` } else { parsedTags.returns = comment } } // Parse example tag else if (tagName === "example") { parsedTags.example = comment } // Store any other tags else { parsedTags[tagName] = comment } }) } // If no description from tags, try to get from JSDoc description if (!description && typeof jsDoc.getDescription === "function") { description = jsDoc.getDescription() || "" } return { description: description.trim(), ...parsedTags, } } } // For variable declarations, check the parent VariableStatement for JSDoc if (typeof node.getParent === "function") { const parent = node.getParent() if (parent && typeof parent.getParent === "function") { const grandparent = parent.getParent() if (grandparent && typeof grandparent.getJsDocs === "function") { const parentJsDocs = grandparent.getJsDocs() if (parentJsDocs && parentJsDocs.length > 0) { const jsDoc = parentJsDocs[0] // Parse tags if available let parsedTags = {} let description = "" if (typeof jsDoc.getTags === "function") { const tags = jsDoc.getTags() tags.forEach(tag => { const tagName = tag.getTagName() let comment = "" // Try to get comment text if (typeof tag.getComment === "function") { comment = tag.getComment() || "" } // Parse different tag types if (tagName === "description") { description = comment } // Parse param tags else if (tagName === "param") { if (!parsedTags.params) parsedTags.params = {} // Try to get parameter name from the tag let paramName = "" if (typeof tag.getName === "function") { paramName = tag.getName() } // If no name found, try to extract from comment if (!paramName && comment) { const paramMatch = comment.match(/^(\w+[\.\w]*)\s+(.*)$/) if (paramMatch) { paramName = paramMatch[1] comment = paramMatch[2] } } if (paramName) { parsedTags.params[paramName] = comment } } // Parse return tag else if (tagName === "returns" || tagName === "return") { // Handle multiple @returns tags by concatenating them if (parsedTags.returns) { parsedTags.returns += `\n• ${comment}` } else { parsedTags.returns = comment } } // Parse example tag else if (tagName === "example") { parsedTags.example = comment } // Store any other tags else { parsedTags[tagName] = comment } }) } // If no description from tags, try to get from JSDoc description if (!description && typeof jsDoc.getDescription === "function") { description = jsDoc.getDescription() || "" } return { description: description.trim(), ...parsedTags, } } } } } // Fallback: try to parse JSDoc from the node leading comments if (typeof node.getLeadingCommentRanges === "function") { const commentRanges = node.getLeadingCommentRanges() if (commentRanges && commentRanges.length > 0) { const commentText = commentRanges .map(range => range.getText()) .join("\n") // Simple regex to extract JSDoc description const match = /\/\*\*\s*([\s\S]*?)\s*\*\//.exec(commentText) if (match && match[1]) { const description = match[1].replace(/^\s*\*\s?/gm, "").trim() return {description} } } } // Also try to get comments from parent node if current node doesn't have any if (typeof node.getParent === "function") { const parent = node.getParent() if (parent && typeof parent.getLeadingCommentRanges === "function") { const commentRanges = parent.getLeadingCommentRanges() if (commentRanges && commentRanges.length > 0) { const commentText = commentRanges .map(range => range.getText()) .join("\n") // Simple regex to extract JSDoc description const match = /\/\*\*\s*([\s\S]*?)\s*\*\//.exec(commentText) if (match && match[1]) { const description = match[1].replace(/^\s*\*\s?/gm, "").trim() return {description} } } } } return {} } catch (e) { console.warn(`Error parsing JSDoc: ${e.message}`) return {} } } module.exports = { parseJsDoc, } ================================================ FILE: docs-generator/utils/namespace-utils.js ================================================ const path = require("path") const {Node} = require("ts-morph") const {extractFunctionInfo} = require("./function-extractor") function extractNamespaceFunctions( sourceFile, namespaceName, importedNamespace ) { const functions = [] try { // Get the imported namespace source file const moduleSpecifier = importedNamespace.getModuleSpecifier() if (!moduleSpecifier) { console.warn(`No module specifier for namespace ${namespaceName}`) return functions } const moduleSpecifierValue = moduleSpecifier.getLiteralValue() // Skip external packages (those starting with @, or not starting with ./) if ( moduleSpecifierValue.startsWith("@") || (!moduleSpecifierValue.startsWith(".") && !moduleSpecifierValue.startsWith("/")) ) { // This is an external package, silently skip return functions } // Find the source file using the same logic as resolveReExportedFunction const referencedSourceFiles = sourceFile.getReferencedSourceFiles() let namespaceSourceFile = null // Find the source file that matches the module specifier for (const sf of referencedSourceFiles) { const fileName = path.basename(sf.getFilePath(), ".ts") const moduleFileName = path.basename(moduleSpecifierValue, ".ts") if ( fileName === moduleFileName || sf.getFilePath().includes(moduleSpecifierValue) || sf.getFilePath().includes(moduleSpecifierValue.replace("./", "")) ) { namespaceSourceFile = sf break } } if (!namespaceSourceFile) { // Only warn for internal modules (those starting with ./ or ../) if ( moduleSpecifierValue.startsWith("./") || moduleSpecifierValue.startsWith("../") ) { console.warn( `Could not find source file for namespace ${namespaceName} from ${moduleSpecifierValue}` ) } return functions } // Get all exported declarations from the namespace namespaceSourceFile .getExportedDeclarations() .forEach((declarations, name) => { declarations.forEach(declaration => { const funcInfo = extractFunctionInfo( declaration, name, namespaceSourceFile, namespaceName ) if (funcInfo) { functions.push(funcInfo) } }) }) } catch (e) { console.warn( `Error extracting functions from namespace ${namespaceName}: ${e.message}` ) } return functions } module.exports = { extractNamespaceFunctions, } ================================================ FILE: docs-generator/utils/type-utils.js ================================================ function cleanupTypeText(typeText) { if (!typeText) return typeText // Remove import paths and keep only the type name let cleaned = typeText.replace(/import\("([^"]+)"\)\.([^.\s<>,\[\]]+)/g, "$2") // Clean up Promise types with imports cleaned = cleaned.replace( /Promise<import\("([^"]+)"\)\.([^.\s<>,\[\]]+)>/g, "Promise<$2>" ) // Remove any remaining file system paths cleaned = cleaned.replace(/\/[^"]*\/([^"\/]+)"/g, '"$1"') // Clean up array types cleaned = cleaned.replace( /import\("([^"]+)"\)\.([^.\s<>,\[\]]+)\[\]/g, "$2[]" ) return cleaned } function stripGenericParams(typeText) { if (!typeText || typeof typeText !== "string") { return typeText } // Strip <T>, <T extends Something>, <T, U>, etc. from type names return typeText.replace(/<[^>]*>/, "") } function toCamelCase(typeName) { if (!typeName) return typeName // Convert PascalCase to camelCase (e.g., AccountProofData -> accountProofData) return typeName.charAt(0).toLowerCase() + typeName.slice(1) } function escapeParameterNameForMDX(paramName) { // Don't escape curly braces in parameter names as they are part of destructuring syntax // and don't need to be escaped in code blocks in MDX return paramName } function escapeTextForMDX(text) { if (!text) return text // Escape angle brackets to prevent MDX from interpreting TypeScript generics as HTML/JSX tags // For example: "Promise<Interaction>" becomes "Promise<Interaction>" return text.replace(/</g, "<").replace(/>/g, ">") } module.exports = { cleanupTypeText, toCamelCase, escapeParameterNameForMDX, escapeTextForMDX, stripGenericParams, } ================================================ FILE: docs-generator/utils/typescript-formatter.js ================================================ function formatTypeScript(typeText) { if (!typeText || typeof typeText !== "string") { return typeText } let formatted = typeText // Add newlines after semicolons in object types .replace(/;\s*/g, ";\n ") // Add newlines after opening braces .replace(/\{\s*/g, "{\n ") // Add newlines before closing braces .replace(/\s*\}/g, "\n}") // Add proper spacing around colons .replace(/\s*:\s*/g, ": ") // Add proper spacing around arrows .replace(/\s*=>\s*/g, " => ") // Add proper spacing around pipes .replace(/\s*\|\s*/g, " | ") // Add proper spacing around ampersands .replace(/\s*&\s*/g, " & ") // Clean up excessive whitespace and newlines formatted = formatted .replace(/\n\s*\n/g, "\n") .replace(/^\s+|\s+$/g, "") .replace(/\n\s*;/g, ";") return formatted } module.exports = { formatTypeScript, } ================================================ FILE: jest.config.js ================================================ module.exports = { projects: ["<rootDir>/packages/*"], } ================================================ FILE: lerna.json ================================================ { "packages": ["packages/*"], "version": "independent" } ================================================ FILE: package.json ================================================ { "workspaces": [ "./packages/*" ], "scripts": { "build": "lerna run build", "start": "npm run build && lerna run start --parallel", "test": "jest", "release": "npm run build && npm run changeset publish", "changeset": "changeset", "clear": "find . -name node_modules -type d -prune -exec rm -rf '{}' + && find . -name dist -type d -prune -exec rm -rf '{}' +", "prettier:check": "prettier --check .", "prettier": "prettier --write .", "generate-all-docs": "node docs-generator/generate-all-docs.js", "demo": "cd packages/demo && npm run dev:emulator", "demo:testnet": "cd packages/demo && npm run dev:testnet" }, "name": "fcl-js", "devDependencies": { "@babel/preset-typescript": "^7.25.7", "@changesets/changelog-github": "^0.4.8", "@changesets/cli": "^2.27.9", "@types/jest": "^29.5.13", "@types/node": "^18.19.57", "@typescript-eslint/eslint-plugin": "^8.57.2", "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.1", "handlebars": "^4.7.8", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "lerna": "^9.0.3", "prettier": "^3.3.3", "prettier-plugin-classnames": "^0.7.3", "prettier-plugin-tailwindcss": "^0.6.8", "ts-jest": "^29.2.5", "ts-morph": "^21.0.1", "typescript": "^5.6.3" }, "optionalDependencies": { "@nx/nx-darwin-arm64": "^17.3.2", "@nx/nx-darwin-x64": "^17.3.2", "@nx/nx-linux-x64-gnu": "^17.3.2", "@nx/nx-win32-x64-msvc": "^17.3.2" }, "dependencies": { "@noble/hashes": "^1.7.1" } } ================================================ FILE: packages/config/.babelrc ================================================ { "presets": [["@babel/preset-env"], "@babel/preset-typescript"] } ================================================ FILE: packages/config/.browserslistrc ================================================ defaults and supports es6-module maintained node versions ================================================ FILE: packages/config/.eslintrc.json ================================================ { "env": { "browser": true, "es2021": true, "jest": true, "node": true }, "extends": [ "plugin:jsdoc/recommended-typescript", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended" ], "plugins": ["jsdoc", "@typescript-eslint"], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, "ignorePatterns": ["**/dist/"], "rules": { "@typescript-eslint/no-explicit-any": "off" } } ================================================ FILE: packages/config/.npmignore ================================================ src/ ================================================ FILE: packages/config/CHANGELOG.md ================================================ # @onflow/config ## 1.11.1 ### Patch Changes - [#2767](https://github.com/onflow/fcl-js/pull/2767) [`9a982e62e45a2f6f75fb566c0ce53ee56d3c447c`](https://github.com/onflow/fcl-js/commit/9a982e62e45a2f6f75fb566c0ce53ee56d3c447c) Thanks [@mfbz](https://github.com/mfbz)! - Trigger Changeset Release workflow. ## 1.11.0 ### Minor Changes - [#2765](https://github.com/onflow/fcl-js/pull/2765) [`0fe606e92ab3103dd29ba6a60c407c438aa86d9e`](https://github.com/onflow/fcl-js/commit/0fe606e92ab3103dd29ba6a60c407c438aa86d9e) Thanks [@mfbz](https://github.com/mfbz)! - Trigger Changeset Release workflow. ## 1.10.0 ### Minor Changes - [#2763](https://github.com/onflow/fcl-js/pull/2763) [`61a68051a6d6732c84a918b298eec5aabd561da7`](https://github.com/onflow/fcl-js/commit/61a68051a6d6732c84a918b298eec5aabd561da7) Thanks [@mfbz](https://github.com/mfbz)! - Trigger Changeset Release workflow. ## 1.9.0 ### Minor Changes - [#2761](https://github.com/onflow/fcl-js/pull/2761) [`aa737c359559d9f9f44d518805d5b1228aad9754`](https://github.com/onflow/fcl-js/commit/aa737c359559d9f9f44d518805d5b1228aad9754) Thanks [@mfbz](https://github.com/mfbz)! - Trigger Changeset Release workflow. ## 1.8.0 ### Minor Changes - [#2759](https://github.com/onflow/fcl-js/pull/2759) [`efad6e114bd53e11347c261aaa0e826a6452fd69`](https://github.com/onflow/fcl-js/commit/efad6e114bd53e11347c261aaa0e826a6452fd69) Thanks [@mfbz](https://github.com/mfbz)! - Trigger Changeset Release workflow. ## 1.7.0 ### Minor Changes - [#2747](https://github.com/onflow/fcl-js/pull/2747) [`197938abdc11798c0299aab3394daa5cf1959f65`](https://github.com/onflow/fcl-js/commit/197938abdc11798c0299aab3394daa5cf1959f65) Thanks [@jribbink](https://github.com/jribbink)! - Add support for fork networks (mainnet-fork, testnet-fork) to enable testing against forked emulator with automatic contract alias inheritance from parent networks. Fork networks allow developers to run their dapps and E2E tests against a local emulator that mirrors mainnet or testnet state, while supporting contract-specific overrides. ## 1.6.3 ### Patch Changes - [#2698](https://github.com/onflow/fcl-js/pull/2698) [`9c5bed0ed542e85d038e1763c6d94e38614d9a0e`](https://github.com/onflow/fcl-js/commit/9c5bed0ed542e85d038e1763c6d94e38614d9a0e) Thanks [@chasefleming](https://github.com/chasefleming)! - Add support for Cadence import aliasing ## 1.6.2 ### Patch Changes - [#2683](https://github.com/onflow/fcl-js/pull/2683) [`c8b0b880d147840c66b8913894a8fe1e9804d557`](https://github.com/onflow/fcl-js/commit/c8b0b880d147840c66b8913894a8fe1e9804d557) Thanks [@jribbink](https://github.com/jribbink)! - Ensure `getContracts` accumulates `flow.json` deployments across multiple accounts ## 1.6.1 ### Patch Changes - [#2634](https://github.com/onflow/fcl-js/pull/2634) [`d5f242b217426f125610f8043aea1a70e143a94a`](https://github.com/onflow/fcl-js/commit/d5f242b217426f125610f8043aea1a70e143a94a) Thanks [@jribbink](https://github.com/jribbink)! - Update dependencies - Updated dependencies [[`d5f242b217426f125610f8043aea1a70e143a94a`](https://github.com/onflow/fcl-js/commit/d5f242b217426f125610f8043aea1a70e143a94a)]: - @onflow/util-actor@1.3.5 - @onflow/util-invariant@1.2.5 - @onflow/util-logger@1.3.4 ## 1.6.0 ### Minor Changes - [#2594](https://github.com/onflow/fcl-js/pull/2594) [`72e23611de8025dbd36bddc2dcc1c5858f50efe8`](https://github.com/onflow/fcl-js/commit/72e23611de8025dbd36bddc2dcc1c5858f50efe8) Thanks [@jribbink](https://github.com/jribbink)! - Expose `getContracts` function ## 1.6.0-alpha.0 ### Minor Changes - [#2594](https://github.com/onflow/fcl-js/pull/2594) [`72e23611de8025dbd36bddc2dcc1c5858f50efe8`](https://github.com/onflow/fcl-js/commit/72e23611de8025dbd36bddc2dcc1c5858f50efe8) Thanks [@jribbink](https://github.com/jribbink)! - Expose `getContracts` function ## 1.5.2 ### Patch Changes - [#2327](https://github.com/onflow/fcl-js/pull/2327) [`f2721d7fffec1f5b8e0f9faac6b633c6d9b86c01`](https://github.com/onflow/fcl-js/commit/f2721d7fffec1f5b8e0f9faac6b633c6d9b86c01) Thanks [@jribbink](https://github.com/jribbink)! - Fix deprecated `punycode` dependency warning ## 1.5.1 ### Patch Changes - [#1983](https://github.com/onflow/fcl-js/pull/1983) [`18d24c8bad7efa0d8741d74f0cf299f89b3622c7`](https://github.com/onflow/fcl-js/commit/18d24c8bad7efa0d8741d74f0cf299f89b3622c7) Thanks [@jribbink](https://github.com/jribbink)! - Update dependencies - Updated dependencies [[`18d24c8bad7efa0d8741d74f0cf299f89b3622c7`](https://github.com/onflow/fcl-js/commit/18d24c8bad7efa0d8741d74f0cf299f89b3622c7)]: - @onflow/util-invariant@1.2.4 - @onflow/util-logger@1.3.3 - @onflow/util-actor@1.3.4 ## 1.5.1-alpha.0 ### Patch Changes - [#1983](https://github.com/onflow/fcl-js/pull/1983) [`18d24c8bad7efa0d8741d74f0cf299f89b3622c7`](https://github.com/onflow/fcl-js/commit/18d24c8bad7efa0d8741d74f0cf299f89b3622c7) Thanks [@jribbink](https://github.com/jribbink)! - Update dependencies - Updated dependencies [[`18d24c8bad7efa0d8741d74f0cf299f89b3622c7`](https://github.com/onflow/fcl-js/commit/18d24c8bad7efa0d8741d74f0cf299f89b3622c7)]: - @onflow/util-invariant@1.2.4-alpha.0 - @onflow/util-logger@1.3.3-alpha.0 - @onflow/util-actor@1.3.4-alpha.0 ## 1.5.0 ### Minor Changes - [`cd234798`](https://github.com/onflow/fcl-js/commit/cd234798008868df13447ea97654b7e278dd746f) Thanks [@nialexsan](https://github.com/nialexsan)! - Add `ignoreConflicts` flag to `config.load` ## 1.5.0-alpha.0 ### Minor Changes - [`cd234798`](https://github.com/onflow/fcl-js/commit/cd234798008868df13447ea97654b7e278dd746f) Thanks [@nialexsan](https://github.com/nialexsan)! - Add `ignoreConflicts` flag to `config.load` ## 1.4.1 ### Patch Changes - Updated dependencies [[`fe5e1b3d`](https://github.com/onflow/fcl-js/commit/fe5e1b3d330b7734740cceb9a873d1b680f28175)]: - @onflow/util-actor@1.3.3 ## 1.4.0 ### Minor Changes - [#1870](https://github.com/onflow/fcl-js/pull/1870) [`ad089fe7`](https://github.com/onflow/fcl-js/commit/ad089fe7556767e1fae96f3f2e98fd76c49bba88) Thanks [@btspoony](https://github.com/btspoony)! - Add previewnet to config.load ### Patch Changes - Updated dependencies [[`6c635f9f`](https://github.com/onflow/fcl-js/commit/6c635f9ff340284845ffe1196965ced7c748294f)]: - @onflow/util-invariant@1.2.3 ## 1.3.0 ### Minor Changes - [#1848](https://github.com/onflow/fcl-js/pull/1848) [`fdd52c45`](https://github.com/onflow/fcl-js/commit/fdd52c45b3a64210c5f716e13aa4d08d3796370c) Thanks [@jribbink](https://github.com/jribbink)! - Add support for new dependencies field in flow.json ### Patch Changes - [#1821](https://github.com/onflow/fcl-js/pull/1821) [`b9c078ce`](https://github.com/onflow/fcl-js/commit/b9c078ce87869c2b41dff07b861cea09a294c6a1) Thanks [@nialexsan](https://github.com/nialexsan)! - Split packages into `@onflow/fcl`, `@onflow/fcl-core`, and `@onflow/fcl-react-native`. - [#1827](https://github.com/onflow/fcl-js/pull/1827) [`e74c4a60`](https://github.com/onflow/fcl-js/commit/e74c4a60f38f366874aa1391ca1c890a7ad3a42a) Thanks [@nialexsan](https://github.com/nialexsan)! - Pin internal dependencies to exact versions - [#1814](https://github.com/onflow/fcl-js/pull/1814) [`0d09d838`](https://github.com/onflow/fcl-js/commit/0d09d8386c2fc472833df7152467d477f36dddc4) Thanks [@jribbink](https://github.com/jribbink)! - Fix type declarations not fully being generated - Updated dependencies [[`b9c078ce`](https://github.com/onflow/fcl-js/commit/b9c078ce87869c2b41dff07b861cea09a294c6a1), [`e74c4a60`](https://github.com/onflow/fcl-js/commit/e74c4a60f38f366874aa1391ca1c890a7ad3a42a), [`0d09d838`](https://github.com/onflow/fcl-js/commit/0d09d8386c2fc472833df7152467d477f36dddc4)]: - @onflow/util-invariant@1.2.2 - @onflow/util-logger@1.3.2 - @onflow/util-actor@1.3.2 ## 1.3.0-alpha.3 ### Minor Changes - [#1848](https://github.com/onflow/fcl-js/pull/1848) [`fdd52c45`](https://github.com/onflow/fcl-js/commit/fdd52c45b3a64210c5f716e13aa4d08d3796370c) Thanks [@jribbink](https://github.com/jribbink)! - Add support for new dependencies field in flow.json ## 1.2.2-alpha.2 ### Patch Changes - [#1827](https://github.com/onflow/fcl-js/pull/1827) [`e74c4a60`](https://github.com/onflow/fcl-js/commit/e74c4a60f38f366874aa1391ca1c890a7ad3a42a) Thanks [@nialexsan](https://github.com/nialexsan)! - pin versions - Updated dependencies [[`e74c4a60`](https://github.com/onflow/fcl-js/commit/e74c4a60f38f366874aa1391ca1c890a7ad3a42a)]: - @onflow/util-invariant@1.2.2-alpha.2 - @onflow/util-logger@1.3.2-alpha.2 - @onflow/util-actor@1.3.2-alpha.2 ## 1.2.2-alpha.1 ### Patch Changes - [#1814](https://github.com/onflow/fcl-js/pull/1814) [`0d09d838`](https://github.com/onflow/fcl-js/commit/0d09d8386c2fc472833df7152467d477f36dddc4) Thanks [@jribbink](https://github.com/jribbink)! - Fix type declarations not fully being generated - Updated dependencies [[`0d09d838`](https://github.com/onflow/fcl-js/commit/0d09d8386c2fc472833df7152467d477f36dddc4)]: - @onflow/util-invariant@1.2.2-alpha.1 - @onflow/util-logger@1.3.2-alpha.1 - @onflow/util-actor@1.3.2-alpha.1 ## 1.2.1 ### Patch Changes - [#1807](https://github.com/onflow/fcl-js/pull/1807) [`9430d723`](https://github.com/onflow/fcl-js/commit/9430d7232c272f4acb55f5bcff7be82cef9704d9) Thanks [@jribbink](https://github.com/jribbink)! - Fix versioning & actor bug - Updated dependencies [[`9430d723`](https://github.com/onflow/fcl-js/commit/9430d7232c272f4acb55f5bcff7be82cef9704d9)]: - @onflow/util-actor@1.3.1 - @onflow/util-invariant@1.2.1 - @onflow/util-logger@1.3.1 ## 1.2.0 ### Minor Changes - [#1801](https://github.com/onflow/fcl-js/pull/1801) [`8881394b`](https://github.com/onflow/fcl-js/commit/8881394bc11fea507e330a4c507ef304fe456c42) Thanks [@nialexsan](https://github.com/nialexsan)! - TS build - [#1801](https://github.com/onflow/fcl-js/pull/1801) [`8881394b`](https://github.com/onflow/fcl-js/commit/8881394bc11fea507e330a4c507ef304fe456c42) Thanks [@nialexsan](https://github.com/nialexsan)! - Add Typescript to @onflow/config ### Patch Changes - Updated dependencies [[`8881394b`](https://github.com/onflow/fcl-js/commit/8881394bc11fea507e330a4c507ef304fe456c42), [`8881394b`](https://github.com/onflow/fcl-js/commit/8881394bc11fea507e330a4c507ef304fe456c42), [`8881394b`](https://github.com/onflow/fcl-js/commit/8881394bc11fea507e330a4c507ef304fe456c42)]: - @onflow/util-invariant@1.2.0 - @onflow/util-logger@1.3.0 - @onflow/util-actor@1.3.0 ## 1.1.2 ### Patch Changes - [#1771](https://github.com/onflow/fcl-js/pull/1771) [`5edbd823`](https://github.com/onflow/fcl-js/commit/5edbd823b1a6d25eb7bb52dc55338f95beae73b1) Thanks [@jribbink](https://github.com/jribbink)! - Fix @onflow/util-logger <-> @onflow/config circular dependency ## 1.1.1 ### Patch Changes - [#1680](https://github.com/onflow/fcl-js/pull/1680) [`6193bd42`](https://github.com/onflow/fcl-js/commit/6193bd420371494a975f19fd001af6af012bd72f) Thanks [@nialexsan](https://github.com/nialexsan)! - Updated dependencies - [#1699](https://github.com/onflow/fcl-js/pull/1699) [`fefb578a`](https://github.com/onflow/fcl-js/commit/fefb578ae96a112164c561fa3e0a490008d8a5f6) Thanks [@chasefleming](https://github.com/chasefleming)! - Throw error in config if keys detected ## 1.1.1-alpha.0 ### Patch Changes - [#1680](https://github.com/onflow/fcl-js/pull/1680) [`6193bd42`](https://github.com/onflow/fcl-js/commit/6193bd420371494a975f19fd001af6af012bd72f) Thanks [@nialexsan](https://github.com/nialexsan)! - Updated dependencies - [#1699](https://github.com/onflow/fcl-js/pull/1699) [`fefb578a`](https://github.com/onflow/fcl-js/commit/fefb578ae96a112164c561fa3e0a490008d8a5f6) Thanks [@chasefleming](https://github.com/chasefleming)! - Throw error in config if keys detected ## 1.1.0 ### Minor Changes - [#1481](https://github.com/onflow/fcl-js/pull/1481) [`e10e3c9c`](https://github.com/onflow/fcl-js/commit/e10e3c9c1f611e7dfd8a0bf7292473c71c2e04b9) Thanks [@chasefleming](https://github.com/chasefleming)! - Remove need to manually add contract placeholders in config by ingesting flow.json with config.load - [#1511](https://github.com/onflow/fcl-js/pull/1511) [`de7ffa47`](https://github.com/onflow/fcl-js/commit/de7ffa4768ea19e9378e7db74c85750b6554027c) Thanks [@justinbarry](https://github.com/justinbarry)! - Put contract addr in `system.contracts.*` namespace - [#1575](https://github.com/onflow/fcl-js/pull/1575) [`bbdeea32`](https://github.com/onflow/fcl-js/commit/bbdeea32f024d6eea4a74c94023e01688a38b6cb) Thanks [@chasefleming](https://github.com/chasefleming)! - Exclude tests from type gen - [#1511](https://github.com/onflow/fcl-js/pull/1511) [`48ff4330`](https://github.com/onflow/fcl-js/commit/48ff43303c30bab86274bd281f6af28affdb2f25) Thanks [@justinbarry](https://github.com/justinbarry)! - Add support for `import "ContractName"` syntax in scripts and transactions. - [#1577](https://github.com/onflow/fcl-js/pull/1577) [`d9a49531`](https://github.com/onflow/fcl-js/commit/d9a495316cd03ed0de99e0f01d1b8850a1f0eec4) Thanks [@chasefleming](https://github.com/chasefleming)! - Add npmignore file for build - [#1570](https://github.com/onflow/fcl-js/pull/1570) [`f68c1433`](https://github.com/onflow/fcl-js/commit/f68c14337b5470b4d79ec682f7bb41ddeae2e020) Thanks [@chasefleming](https://github.com/chasefleming)! - Add types for config package. ### Patch Changes - [#1663](https://github.com/onflow/fcl-js/pull/1663) [`62dfafa9`](https://github.com/onflow/fcl-js/commit/62dfafa9c7adc3933822b0d3171d6eb025f1719e) Thanks [@nialexsan](https://github.com/nialexsan)! - Upgrade jest to v29.5 and update tests accordingly. Change build to transpile with ESM modules. - [#1658](https://github.com/onflow/fcl-js/pull/1658) [`2512b5c5`](https://github.com/onflow/fcl-js/commit/2512b5c53dff708fca97cd8afdbb1f4a46b2f106) Thanks [@nialexsan](https://github.com/nialexsan)! - Align jest version - [#1629](https://github.com/onflow/fcl-js/pull/1629) [`544d8ebb`](https://github.com/onflow/fcl-js/commit/544d8ebb298ce1be8491d5609729110211b83242) Thanks [@chasefleming](https://github.com/chasefleming)! - Fix key warning when key/file/location format is used in flow.json - [#1604](https://github.com/onflow/fcl-js/pull/1604) [`a4a1c7bf`](https://github.com/onflow/fcl-js/commit/a4a1c7bf0be9facb213f56a91d1a66b60bdea64b) Thanks [@chasefleming](https://github.com/chasefleming)! - Fix config types when invoked as a function - Updated dependencies [[`62dfafa9`](https://github.com/onflow/fcl-js/commit/62dfafa9c7adc3933822b0d3171d6eb025f1719e), [`2512b5c5`](https://github.com/onflow/fcl-js/commit/2512b5c53dff708fca97cd8afdbb1f4a46b2f106), [`35052784`](https://github.com/onflow/fcl-js/commit/3505278418e64045248c04fd21f0c09ddbb3132e), [`d9a49531`](https://github.com/onflow/fcl-js/commit/d9a495316cd03ed0de99e0f01d1b8850a1f0eec4)]: - @onflow/util-actor@1.2.0 ## 1.1.0-alpha.8 ### Patch Changes - [#1663](https://github.com/onflow/fcl-js/pull/1663) [`62dfafa9`](https://github.com/onflow/fcl-js/commit/62dfafa9c7adc3933822b0d3171d6eb025f1719e) Thanks [@nialexsan](https://github.com/nialexsan)! - Upgrade jest to v29.5 and update tests accordingly. Change build to transpile with ESM modules. - Updated dependencies [[`62dfafa9`](https://github.com/onflow/fcl-js/commit/62dfafa9c7adc3933822b0d3171d6eb025f1719e)]: - @onflow/util-actor@1.2.0-alpha.3 ## 1.1.0-alpha.7 ### Patch Changes - [#1658](https://github.com/onflow/fcl-js/pull/1658) [`2512b5c5`](https://github.com/onflow/fcl-js/commit/2512b5c53dff708fca97cd8afdbb1f4a46b2f106) Thanks [@nialexsan](https://github.com/nialexsan)! - Align jest version - Updated dependencies [[`2512b5c5`](https://github.com/onflow/fcl-js/commit/2512b5c53dff708fca97cd8afdbb1f4a46b2f106)]: - @onflow/util-actor@1.2.0-alpha.2 ## 1.1.0-alpha.6 ### Patch Changes - [#1629](https://github.com/onflow/fcl-js/pull/1629) [`544d8ebb`](https://github.com/onflow/fcl-js/commit/544d8ebb298ce1be8491d5609729110211b83242) Thanks [@chasefleming](https://github.com/chasefleming)! - Fix key warning when key/file/location format is used in flow.json - Updated dependencies [[`35052784`](https://github.com/onflow/fcl-js/commit/3505278418e64045248c04fd21f0c09ddbb3132e)]: - @onflow/util-actor@1.2.0-alpha.1 ## 1.1.0-alpha.5 ### Patch Changes - [#1604](https://github.com/onflow/fcl-js/pull/1604) [`a4a1c7bf`](https://github.com/onflow/fcl-js/commit/a4a1c7bf0be9facb213f56a91d1a66b60bdea64b) Thanks [@chasefleming](https://github.com/chasefleming)! - Fix config types when invoked as a function ## 1.1.0-alpha.4 ### Minor Changes - [#1577](https://github.com/onflow/fcl-js/pull/1577) [`d9a49531`](https://github.com/onflow/fcl-js/commit/d9a495316cd03ed0de99e0f01d1b8850a1f0eec4) Thanks [@chasefleming](https://github.com/chasefleming)! - Add npmignore file for build ### Patch Changes - Updated dependencies [[`d9a49531`](https://github.com/onflow/fcl-js/commit/d9a495316cd03ed0de99e0f01d1b8850a1f0eec4)]: - @onflow/util-actor@1.2.0-alpha.0 ## 1.1.0-alpha.3 ### Minor Changes - [#1575](https://github.com/onflow/fcl-js/pull/1575) [`bbdeea32`](https://github.com/onflow/fcl-js/commit/bbdeea32f024d6eea4a74c94023e01688a38b6cb) Thanks [@chasefleming](https://github.com/chasefleming)! - Exclude tests from type gen ## 1.1.0-alpha.2 ### Minor Changes - [#1570](https://github.com/onflow/fcl-js/pull/1570) [`f68c1433`](https://github.com/onflow/fcl-js/commit/f68c14337b5470b4d79ec682f7bb41ddeae2e020) Thanks [@chasefleming](https://github.com/chasefleming)! - Add types for config package. ## 1.1.0-alpha.1 ### Minor Changes - [#1511](https://github.com/onflow/fcl-js/pull/1511) [`de7ffa47`](https://github.com/onflow/fcl-js/commit/de7ffa4768ea19e9378e7db74c85750b6554027c) Thanks [@justinbarry](https://github.com/justinbarry)! - Put contract addr in `system.contracts.*` namespace - [#1511](https://github.com/onflow/fcl-js/pull/1511) [`48ff4330`](https://github.com/onflow/fcl-js/commit/48ff43303c30bab86274bd281f6af28affdb2f25) Thanks [@justinbarry](https://github.com/justinbarry)! - Add support for `import "ContractName"` syntax in scripts and transactions. ## 1.1.0-alpha.0 ### Minor Changes - [#1481](https://github.com/onflow/fcl-js/pull/1481) [`e10e3c9c`](https://github.com/onflow/fcl-js/commit/e10e3c9c1f611e7dfd8a0bf7292473c71c2e04b9) Thanks [@chasefleming](https://github.com/chasefleming)! - Remove need to manually add contract placeholders in config by ingesting flow.json with config.load ## 1.0.5 ### Patch Changes - [#1443](https://github.com/onflow/fcl-js/pull/1443) [`7bdfa016`](https://github.com/onflow/fcl-js/commit/7bdfa016823d1caac23143351940b42f65d4d1c4) Thanks [@huyndo](https://github.com/huyndo)! - PKG - [config] Fix config overload prone to race condition ## 1.0.4 ### Patch Changes - [#1436](https://github.com/onflow/fcl-js/pull/1436) [`87771cd6`](https://github.com/onflow/fcl-js/commit/87771cd6db2cea13787502522a292e75ce43c4f0) Thanks [@justinbarry](https://github.com/justinbarry)! - Upgrade @onflow/fcl-bundle 1.2.0-alpha.0 -> 1.2.0 - Updated dependencies [[`87771cd6`](https://github.com/onflow/fcl-js/commit/87771cd6db2cea13787502522a292e75ce43c4f0)]: - @onflow/util-actor@1.1.2 ## 1.0.3 ### Patch Changes - [#1227](https://github.com/onflow/fcl-js/pull/1227) [`352f1460`](https://github.com/onflow/fcl-js/commit/352f1460a2f34d228a74fa4bbc6fcf6e68a968b6) Thanks [@jribbink](https://github.com/jribbink)! - Switch to fcl-bundle instead of microbundle for build scripts - Updated dependencies [[`352f1460`](https://github.com/onflow/fcl-js/commit/352f1460a2f34d228a74fa4bbc6fcf6e68a968b6)]: - @onflow/util-actor@1.1.1 ## 1.0.3-alpha.0 ### Patch Changes - [#1227](https://github.com/onflow/fcl-js/pull/1227) [`352f1460`](https://github.com/onflow/fcl-js/commit/352f1460a2f34d228a74fa4bbc6fcf6e68a968b6) Thanks [@jribbink](https://github.com/jribbink)! - Switch to fcl-bundle instead of microbundle for build scripts - Updated dependencies [[`352f1460`](https://github.com/onflow/fcl-js/commit/352f1460a2f34d228a74fa4bbc6fcf6e68a968b6)]: - @onflow/util-actor@1.1.1-alpha.0 ## 1.0.2 ### Patch Changes - Updated dependencies [[`4ec2bdc9`](https://github.com/onflow/fcl-js/commit/4ec2bdc9805ac081bdc8003b6e1ea52e02d3909d)]: - @onflow/util-actor@1.1.0 ## 1.0.2-alpha.0 ### Patch Changes - Updated dependencies [[`4ec2bdc9`](https://github.com/onflow/fcl-js/commit/4ec2bdc9805ac081bdc8003b6e1ea52e02d3909d)]: - @onflow/util-actor@1.1.0-alpha.0 ## 1.0.1 ### Patch Changes - [#1178](https://github.com/onflow/fcl-js/pull/1178) [`9e7e4cfb`](https://github.com/onflow/fcl-js/commit/9e7e4cfbc026765019653b0e891e63a2d789ceb4) Thanks [@jribbink](https://github.com/jribbink)! - Add --no-compress to watch scripts for easier debugging - Updated dependencies [[`9e7e4cfb`](https://github.com/onflow/fcl-js/commit/9e7e4cfbc026765019653b0e891e63a2d789ceb4)]: - @onflow/util-actor@1.0.1 ## 1.0.0 ### Patch Changes - [#1124](https://github.com/onflow/fcl-js/pull/1124) [`9c191c15`](https://github.com/onflow/fcl-js/commit/9c191c1520ee772b4343265a42ad0e995a92dd9a) Thanks [@chasefleming](https://github.com/chasefleming)! - Create config package * [#1164](https://github.com/onflow/fcl-js/pull/1164) [`11229868`](https://github.com/onflow/fcl-js/commit/11229868cf916d204901f8bb3f76ee234e9152a8) Thanks [@justinbarry](https://github.com/justinbarry)! - No longer minify released source code. * Updated dependencies [[`de47af64`](https://github.com/onflow/fcl-js/commit/de47af647a5bdad154a2d83e2ea2260ab54f0c60), [`11229868`](https://github.com/onflow/fcl-js/commit/11229868cf916d204901f8bb3f76ee234e9152a8), [`ced27ea8`](https://github.com/onflow/fcl-js/commit/ced27ea855988f02f1312c7b732aa107a410c854)]: - @onflow/util-actor@1.0.0 ## 1.0.0-alpha.2 ### Patch Changes - [#1164](https://github.com/onflow/fcl-js/pull/1164) [`11229868`](https://github.com/onflow/fcl-js/commit/11229868cf916d204901f8bb3f76ee234e9152a8) Thanks [@justinbarry](https://github.com/justinbarry)! - No longer minify released source code. - Updated dependencies [[`11229868`](https://github.com/onflow/fcl-js/commit/11229868cf916d204901f8bb3f76ee234e9152a8)]: - @onflow/util-actor@1.0.0-alpha.2 ## 1.0.0-alpha.1 ### Patch Changes - [#1124](https://github.com/onflow/fcl-js/pull/1124) [`9c191c15`](https://github.com/onflow/fcl-js/commit/9c191c1520ee772b4343265a42ad0e995a92dd9a) Thanks [@chasefleming](https://github.com/chasefleming)! - Create config package - Updated dependencies [[`de47af64`](https://github.com/onflow/fcl-js/commit/de47af647a5bdad154a2d83e2ea2260ab54f0c60)]: - @onflow/util-actor@1.0.0-alpha.1 ================================================ FILE: packages/config/README.md ================================================ # @onflow/config Reactive configuration for Flow JS SDK and FCL ## Installation ```bash npm install @onflow/config ``` ## Usage ```javascript import {config} from "@onflow/sdk" // Reactively subscribe to config changes config().subscribe(configData => console.log("CONFIG", configData)) // Set a config value config().put("foo", "bar") // .put can be chained config() .put("foo", "bar") .put("baz", "rawr") // Get a config value (it's async) var configValue = await config().get("woot") console.log(configValue) // undefined // A fallback can be supplied for .get var configValue = await config().get("woot", "fallback") console.log(configValue) // "fallback" config.put("woot", "woot") var configValue = await config().get("woot", "fallback") console.log(configValue) // "woot" // Update a config value config().put("count", 1) var count = await config().get("count", 0) console.log(count) // 1 config().update("count", oldValue => oldValue + 1) var count = await config().get("count", 0) console.log(count) // 2 // Delete a config value config().delete("woot") var configValue = await config().get("woot", "fallback") console.log(configValue) // "fallback" // Configs that match a pattern config() .put("scope.A", 1) .put("scope.B", 1) var scopeValues = await config().where(/^scope\.\s+/) console.log(scopeValues) // { "scope.A": 1, "scope.B": 2 } ``` ## Loading flow.json Before loading a `flow.json`, you must set the network: ```javascript import { config } from "@onflow/config" // Set network (required before loading flow.json) config().put("flow.network", "testnet") // Load flow.json await config().load({ flowJSON: require('./flow.json') }) ``` ## Import Aliases Import aliases allow you to deploy the same contract to multiple addresses with different names, enabling version management and multi-instance deployments. ### flow.json Configuration ```json { "contracts": { "FUSD": { "source": "./contracts/FUSD.cdc", "aliases": { "testnet": "0x9a0766d93b6608b7" } }, "FUSD1": { "source": "./contracts/FUSD.cdc", "aliases": { "testnet": "0xe223d8a629e49c68" }, "canonical": "FUSD" } } } ``` ### How It Works When `config.load(flowJSON)` is called: 1. **Contract addresses are extracted** based on the current network 2. **Canonical references are extracted** from the `canonical` field 3. **Values are stored in config**: ```typescript system.contracts.FUSD = "0x9a0766d93b6608b7" system.contracts.FUSD1 = "0xe223d8a629e49c68" system.contracts.FUSD1.canonical = "FUSD" ``` ### Import Resolution When resolving Cadence imports: - `import "FUSD"` → `import FUSD from 0x9a0766d93b6608b7` (no canonical) - `import "FUSD1"` → `import FUSD as FUSD1 from 0xe223d8a629e49c68` (with canonical) The `canonical` field tells the resolver that `FUSD1` is an alias of the `FUSD` contract, so it generates the `import X as Y` syntax. ### Use Cases **Multiple Versions:** ```json { "contracts": { "FungibleToken": { "source": "./contracts/FungibleToken.cdc", "aliases": { "testnet": "0xf233dcee88fe0abe" } }, "FungibleTokenV2": { "source": "./contracts/FungibleToken.cdc", "aliases": { "testnet": "0x9a0766d93b6608b7" }, "canonical": "FungibleToken" } } } ``` **Multiple Instances:** ```json { "contracts": { "Token1": { "source": "./contracts/Token.cdc", "aliases": { "testnet": "0x1111" }, "canonical": "Token" }, "Token2": { "source": "./contracts/Token.cdc", "aliases": { "testnet": "0x2222" }, "canonical": "Token" } } } ``` ### Dependencies The `canonical` field also works with dependencies: ```json { "dependencies": { "FungibleTokenV2": { "source": "mainnet://f233dcee88fe0abe.FungibleToken", "hash": "abc123...", "aliases": { "testnet": "0xf233dcee88fe0abe" }, "canonical": "FungibleToken" } } } ``` ## Known Configuration Values ### Access Node - `accessNode.api` _(default: emulator url)_ -- Where FCL will communicate with the Flow blockchain. - `accessNode.key` _(default: null)_ -- Some Access Nodes require an API key. ```javascript import {config} from "@onflow/fcl" if (process.env.NODE_ENV === "production") { config() .put("accessNode.api", process.env.ACCESS_NODE_API) .put("accessNode.key", process.env.ACCESS_NODE_KEY) } ``` ### Decode `decoder.*` -- Custom decoders for parsing JSON-CDC ```javascript import {config, query} from "@onflow/fcl" function Woot({x, y}) { this.x = x this.y = y } config() .put("decoder.Woot", woot => new Woot(woot)) var data = await fcl.query({ cadence: ` pub struct Woot { pub var x: Int pub var y: Int init(x: Int, y: Int) { self.x = x self.y = y } } pub fun main(): [Woot] { return [Woot(x: 1, y: 2), Woot(x: 3, y: 4), Woot(x: 5, y: 6)] } ` }) console.log(data) // [ Woot{x:1, y:2}, Woot{x:3, y:4}, Woot{x:5, y:6} ] ``` ### Wallets `wallet.discovery` _(default: FCL wallet discovery service url)_ -- Where FCL will attempt to authenticate ```javascript import {config} from "@onflow/fcl" if (process.env.NODE_ENV === "development") { // Use dev wallet during development config() .put("discovery.wallet", "http://localhost:8701/flow/authenticate") } ``` ## API Reference ### `config()` Returns the config instance. ### `config().put(key, value)` Sets a configuration value. Can be chained. ### `config().get(key, fallback?)` Gets a configuration value, optionally with a fallback. Returns a Promise. ### `config().update(key, updateFn)` Updates a configuration value using a function that receives the old value. ### `config().load({ flowJSON })` Loads contract addresses and canonical references from a flow.json file. **Parameters:** - `flowJSON`: Flow JSON object or array of Flow JSON objects **Requirements:** - `flow.network` must be set before calling `load()` ### `config().delete(key)` Deletes a configuration value. ### `config().all()` Returns all configuration values as an object. ### `config().where(pattern)` Returns configuration values matching a regex pattern. ### `config().subscribe(callback)` Subscribes to configuration changes. Returns an unsubscribe function. ## See Also - [Flow CLI Documentation](https://developers.flow.com/tools/flow-cli) - [Flow JavaScript SDK](https://developers.flow.com/tools/fcl-js) - [flow.json Configuration](https://developers.flow.com/tools/flow-cli/flow.json) ================================================ FILE: packages/config/package.json ================================================ { "name": "@onflow/config", "version": "1.11.1", "description": "Config for FCL-JS", "license": "Apache-2.0", "author": "Flow Foundation", "homepage": "https://flow.com", "repository": { "type": "git", "url": "git+ssh://git@github.com/onflow/fcl-js.git" }, "bugs": { "url": "https://github.com/onflow/fcl-js/issues" }, "devDependencies": { "@babel/preset-typescript": "^7.25.7", "@onflow/fcl-bundle": "1.7.1", "@types/estree": "^1.0.6", "@types/jest": "^29.5.13", "@typescript-eslint/eslint-plugin": "^8.57.2", "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.1", "eslint-plugin-jsdoc": "^46.10.1", "jest": "^29.7.0", "typescript": "^4.9.5" }, "source": "src/config.ts", "main": "dist/config.js", "module": "dist/config.module.js", "unpkg": "dist/config.umd.js", "types": "types/config.d.ts", "scripts": { "prepublishOnly": "npm test && npm run build", "test": "jest", "build": "npm run lint && fcl-bundle", "test:watch": "jest --watch", "start": "fcl-bundle --watch", "lint": "eslint src" }, "dependencies": { "@babel/runtime": "^7.25.7", "@onflow/util-actor": "1.3.5", "@onflow/util-invariant": "1.2.5", "@onflow/util-logger": "1.3.4" } } ================================================ FILE: packages/config/src/config.test.ts ================================================ import {config, clearConfig} from "./config" const idle = () => new Promise(resolve => setTimeout(resolve, 0)) describe("config()", () => { beforeEach(async () => { clearConfig() }) describe("crud methods", () => { beforeEach(async () => { config({ "config.test.init": "rawr", }) config() .put("config.test.t", "t") .put("config.test.z", "z") .put("config.test.foo.bar", "bar") .put("config.test.foo.baz", "baz") .put("config.test.wat.bar", "foo") }) test("get", async () => { expect(await config().get("config.test.foo.bar")).toBe("bar") expect(await config().get("config.test.init")).toBe("rawr") }) test("get with fallback", async () => { expect(await config().get("config.test.not.a.thing", "fallback")).toBe( "fallback" ) }) test("update", async () => { config().update("config.test.t", (v: number) => v + v) expect(await config().get("config.test.t")).toBe("tt") }) test("delete", async () => { config().delete("config.test.z") expect(await config().get("config.test.z")).toBe(undefined) }) test("where", async () => { expect(await config().where(/^config.test.foo/)).toEqual({ "config.test.foo.bar": "bar", "config.test.foo.baz": "baz", }) }) test("subscribe", async () => { const fn1 = jest.fn() const unsub = config().subscribe(fn1) await idle() config().put("config.test.y", "y").put("config.test.x", "x") await idle() unsub() await idle() config().update("config.test.y", (v: number) => v + v) await idle() expect(fn1).toHaveBeenCalledTimes(3) }) test("all", async () => { expect(await config().all()).toEqual({ "config.test.foo.bar": "bar", "config.test.foo.baz": "baz", "config.test.init": "rawr", "config.test.t": "t", "config.test.wat.bar": "foo", "config.test.z": "z", }) }) }) test("empty", async () => { clearConfig() await idle() expect(await config().all()).toEqual({}) }) describe("sans ()", () => { test("config(data)", async () => { const data = { foo: "bar", baz: "buz", } config(data) expect(await config.all()).toEqual(data) }) test("config.put config.get", async () => { config.put("foo", "bar") expect(await config.get("foo")).toBe("bar") }) }) describe("overload", () => { test("overload", async () => { const PRE = { yes: "yes", foo: "bar", bar: "baz", } const POST = { foo: "bar!!", bar: "baz!!", omg: "omg!!", } config(PRE) expect(await config.all()).toEqual(PRE) const ret = await config.overload(POST, async () => { expect(await config.all()).toEqual({...PRE, ...POST}) return "WOOT WOOT" }) expect(ret).toBe("WOOT WOOT") expect(await config.all()).toEqual(PRE) }) }) describe("first", () => { const A = "A", B = null, C = 0, D = {} const FALLBACK = "FALLBACK" beforeEach(() => config({A: A, B: B, C: C, D: D})) afterEach(clearConfig) const examples: ([any, string[]] | [any])[] = [ [FALLBACK], [A, ["A"]], [FALLBACK, ["B"]], [C, ["C"]], [D, ["D"]], [A, ["MISSING", "A"]], [FALLBACK, ["MISSING", "B"]], ] for (const [i, [want, from]] of examples.entries()) { test(`Example ${i}: ${from} -> ${want}`, async () => { expect(await config.first(from, FALLBACK)).toBe(want) }) } }) describe("load method", () => { describe("with a set network", () => { beforeEach(() => { // Just picked a random network. Behavior might differ based on network selection at a future date. config().put("flow.network", "emulator") }) describe("flow.json v1", () => { let flowJSON describe("without aliases", () => { beforeEach(async () => { flowJSON = { accounts: { "emulator-account": { fromFile: "./emulator.private.json", }, }, contracts: { HelloWorld: { source: "./cadence/contracts/HelloWorld.cdc", aliases: { emulator: "0x1", testnet: "0x01", mainnet: "0x001", }, }, SecondLife: { aliases: { testnet: "0x02", mainnet: "0x002", }, }, }, deployments: { emulator: { "emulator-account": ["HelloWorld"], }, }, networks: { emulator: "127.0.0.1:3569", }, } }) describe("with single config loaded", () => { beforeEach(async () => { await config().load({flowJSON}) await idle() }) test("should load the contract location wrt the currently set network", async () => { await expect(config().get("0xHelloWorld")).resolves.toBe("0x1") await expect( config().get("system.contracts.HelloWorld") ).resolves.toBe("0x1") }) test("should not set a contract if it does not have an alias", async () => { await expect( config().get("0xSecondLife") ).resolves.toBeUndefined() await expect( config().get("system.contracts.SecondLife") ).resolves.toBeUndefined() }) }) describe("with an array of configs loaded", () => { beforeEach(async () => { const secondFlowJSON = { accounts: {}, contracts: { ThirdContract: { aliases: { emulator: "0x3", testnet: "0x03", mainnet: "0x003", }, }, }, deployments: {}, networks: {}, } await config().load({flowJSON: [flowJSON, secondFlowJSON]}) await idle() }) test("should load the contract locations from the secondFlowJSON", async () => { await expect(config().get("0xThirdContract")).resolves.toBe("0x3") await expect( config().get("system.contracts.ThirdContract") ).resolves.toBe("0x3") }) }) }) describe("without contract aliases", () => { beforeEach(async () => { flowJSON = { networks: { emulator: "127.0.0.1:3569", }, accounts: { default: { address: "f8d6e0586b0a20c7", key: "ba68d45a5acaa52f3cacf4ad3a64d9523e0ce0ae3addb1ee6805385b380b7646", }, }, contracts: { HelloWorld: "./cadence/contracts/HelloWorld.cdc", }, deployments: { emulator: { default: ["HelloWorld"], }, }, } await config().load({flowJSON}) await idle() }) test("should resolve to the default address through the deployment link", async () => { await expect(config().get("0xHelloWorld")).resolves.toBe( "f8d6e0586b0a20c7" ) await expect( config().get("system.contracts.HelloWorld") ).resolves.toBe("f8d6e0586b0a20c7") }) }) describe("with both contract aliases and deployments", () => { beforeEach(async () => { flowJSON = { networks: { emulator: "127.0.0.1:3569", }, accounts: { default: { address: "f8d6e0586b0a20c7", key: "ba68d45a5acaa52f3cacf4ad3a64d9523e0ce0ae3addb1ee6805385b380b7646", }, }, contracts: { HelloWorld: { source: "./cadence/contracts/HelloWorld.cdc", aliases: { emulator: "0x1", }, }, }, deployments: { emulator: { default: ["HelloWorld"], }, }, } await config().load({flowJSON}) await idle() }) test("should return the alias, not the deployment address", async () => { await expect(config().get("0xHelloWorld")).resolves.toBe("0x1") await expect( config().get("system.contracts.HelloWorld") ).resolves.toBe("0x1") }) }) describe("with dependencies aliases", () => { beforeEach(async () => { flowJSON = { networks: { emulator: "127.0.0.1:3569", }, accounts: { default: { address: "f8d6e0586b0a20c7", key: "ba68d45a5acaa52f3cacf4ad3a64d9523e0ce0ae3addb1ee6805385b380b7646", }, }, dependencies: { FlowToken: { source: "mainnet://1654653399040a61.FlowToken", hash: "7e2bb5acf84ddcd8ad4a9ddb1e3595c8148ac0a711551e27aa7231db51b2d7ce", aliases: { emulator: "0x1", mainnet: "1654653399040a61", testnet: "7e60df042a9c0868", }, }, }, } await config().load({flowJSON}) await idle() }) test("should return the alias", async () => { await expect(config().get("0xFlowToken")).resolves.toBe("0x1") await expect( config().get("system.contracts.FlowToken") ).resolves.toBe("0x1") }) }) describe("with dependencies aliases and contract aliases", () => { beforeEach(async () => { flowJSON = { networks: { emulator: "127.0.0.1:3569", }, accounts: { default: { address: "f8d6e0586b0a20c7", key: "ba68d45a5acaa52f3cacf4ad3a64d9523e0ce0ae3addb1ee6805385b380b7646", }, }, contracts: { FlowToken: { source: "./cadence/contracts/FlowToken.cdc", aliases: { emulator: "0x1", }, }, }, dependencies: { FlowToken: { source: "mainnet://1654653399040a61.FlowToken", hash: "7e2bb5acf84ddcd8ad4a9ddb1e3595c8148ac0a711551e27aa7231db51b2d7ce", aliases: { emulator: "0x2", mainnet: "1654653399040a61", testnet: "7e60df042a9c0868", }, }, }, } await config().load({flowJSON}) await idle() }) test("should return the dependencies alias, not the contract alias", async () => { await expect(config().get("0xFlowToken")).resolves.toBe("0x2") await expect( config().get("system.contracts.FlowToken") ).resolves.toBe("0x2") }) }) describe("with canonical field", () => { describe("contract with canonical", () => { beforeEach(async () => { flowJSON = { networks: { emulator: "127.0.0.1:3569", }, contracts: { FUSD1: { source: "./contracts/FUSD.cdc", aliases: { emulator: "0xe223d8a629e49c68", }, canonical: "FUSD", }, }, } await config().load({flowJSON}) await idle() }) test("should set contract address", async () => { await expect( config().get("system.contracts.FUSD1") ).resolves.toBe("0xe223d8a629e49c68") await expect(config().get("0xFUSD1")).resolves.toBe( "0xe223d8a629e49c68" ) }) test("should set canonical reference", async () => { await expect( config().get("system.contracts.FUSD1.canonical") ).resolves.toBe("FUSD") }) }) describe("multiple contracts with same canonical", () => { beforeEach(async () => { flowJSON = { networks: { emulator: "127.0.0.1:3569", }, contracts: { FUSD1: { source: "./contracts/FUSD.cdc", aliases: { emulator: "0xe223d8a629e49c68", }, canonical: "FUSD", }, FUSD2: { source: "./contracts/FUSD.cdc", aliases: { emulator: "0x0f9df91c9121c460", }, canonical: "FUSD", }, }, } await config().load({flowJSON}) await idle() }) test("should set both contract addresses", async () => { await expect( config().get("system.contracts.FUSD1") ).resolves.toBe("0xe223d8a629e49c68") await expect( config().get("system.contracts.FUSD2") ).resolves.toBe("0x0f9df91c9121c460") }) test("should set both canonical references", async () => { await expect( config().get("system.contracts.FUSD1.canonical") ).resolves.toBe("FUSD") await expect( config().get("system.contracts.FUSD2.canonical") ).resolves.toBe("FUSD") }) }) describe("contract without canonical field", () => { beforeEach(async () => { flowJSON = { networks: { emulator: "127.0.0.1:3569", }, contracts: { RegularContract: { source: "./contracts/Regular.cdc", aliases: { emulator: "0x123456", }, }, }, } await config().load({flowJSON}) await idle() }) test("should set contract address", async () => { await expect( config().get("system.contracts.RegularContract") ).resolves.toBe("0x123456") }) test("should not set canonical reference", async () => { await expect( config().get("system.contracts.RegularContract.canonical") ).resolves.toBeUndefined() }) }) describe("dependency with canonical field", () => { beforeEach(async () => { flowJSON = { networks: { emulator: "127.0.0.1:3569", }, dependencies: { FungibleTokenV2: { source: "mainnet://f233dcee88fe0abe.FungibleToken", hash: "abc123", aliases: { emulator: "0xf233dcee88fe0abe", }, canonical: "FungibleToken", }, }, } await config().load({flowJSON}) await idle() }) test("should set dependency address", async () => { await expect( config().get("system.contracts.FungibleTokenV2") ).resolves.toBe("0xf233dcee88fe0abe") }) test("should set canonical reference", async () => { await expect( config().get("system.contracts.FungibleTokenV2.canonical") ).resolves.toBe("FungibleToken") }) }) }) }) }) }) describe("without a network set", () => { test("should throw an error", async () => { await expect(() => config().load({flowJSON: {}})).rejects.toThrowError() }) }) }) ================================================ FILE: packages/config/src/config.ts ================================================ import { spawn, send, subscriber, SUBSCRIBE, UNSUBSCRIBE, ActorHandlers, } from "@onflow/util-actor" import * as logger from "@onflow/util-logger" import {invariant} from "@onflow/util-invariant" import {getContracts, cleanNetwork, anyHasPrivateKeys} from "./utils/utils" // Inject config into logger to break circular dependency logger.setConfig(config) const NAME = "config" const PUT = "PUT_CONFIG" const GET = "GET_CONFIG" const GET_ALL = "GET_ALL_CONFIG" const UPDATE = "UPDATE_CONFIG" const DELETE = "DELETE_CONFIG" const CLEAR = "CLEAR_CONFIG" const WHERE = "WHERE_CONFIG" const UPDATED = "CONFIG/UPDATED" const identity = <T>(v: T) => v const HANDLERS: ActorHandlers = { [PUT]: (ctx, _letter, {key, value}) => { if (key == null) throw new Error("Missing 'key' for config/put.") ctx.put(key, value) ctx.broadcast(UPDATED, {...ctx.all()}) }, [GET]: (ctx, letter, {key, fallback}) => { if (key == null) throw new Error("Missing 'key' for config/get") letter.reply(ctx.get(key, fallback)) }, [GET_ALL]: (ctx, letter) => { letter.reply({...ctx.all()}) }, [UPDATE]: (ctx, letter, {key, fn}) => { if (key == null) throw new Error("Missing 'key' for config/update") ctx.update(key, fn || identity) ctx.broadcast(UPDATED, {...ctx.all()}) }, [DELETE]: (ctx, letter, {key}) => { if (key == null) throw new Error("Missing 'key' for config/delete") ctx.delete(key) ctx.broadcast(UPDATED, {...ctx.all()}) }, [CLEAR]: ctx => { const keys = Object.keys(ctx.all()) for (const key of keys) ctx.delete(key) ctx.broadcast(UPDATED, {...ctx.all()}) }, [WHERE]: (ctx, letter, {pattern}) => { if (pattern == null) throw new Error("Missing 'pattern' for config/where") letter.reply(ctx.where(pattern)) }, [SUBSCRIBE]: (ctx, letter) => { ctx.subscribe(letter.from) ctx.send(letter.from, UPDATED, {...ctx.all()}) }, [UNSUBSCRIBE]: (ctx, letter) => { ctx.unsubscribe(letter.from) }, } spawn(HANDLERS, NAME) /** * @description Adds a key-value pair to the config * @param key - The key to add * @param value - The value to add * @returns The config object */ function put<T>(key: string, value: T) { send(NAME, PUT, {key, value}) return config() } /** * @description Gets a key-value pair with a fallback from the config * @param key - The key to add * @param fallback - The fallback value to return if key is not found * @returns The value found at key or fallback */ function get<T>(key: string, fallback?: T): Promise<T> { return send(NAME, GET, {key, fallback}, {expectReply: true, timeout: 10}) } /** * @description Returns the first non null config value or the fallback * @param wants - The keys to search for * @param fallback - The fallback value to return if key is not found * @returns The value found at key or fallback */ async function first<T>(wants: string[] = [], fallback: T): Promise<T> { if (!wants.length) return fallback const [head, ...rest] = wants const ret = await get<T>(head) if (ret == null) return first(rest, fallback) return ret } /** * @description Returns the current config * @returns The config object */ function all(): Promise<Record<string, unknown>> { return send(NAME, GET_ALL, null, {expectReply: true, timeout: 10}) } /** * @description Updates a key-value pair in the config * @param key - The key to update * @param fn - The function to update the value with * @returns The config object */ function update<T>(key: string, fn: (x: T) => T = identity) { send(NAME, UPDATE, {key, fn}) return config() } /** * @description Deletes a key-value pair from the config * @param key - The key to delete * @returns The config object */ function _delete(key: string) { send(NAME, DELETE, {key}) return config() } /** * @description Returns a subset of the config based on a pattern * @param pattern - The pattern to match keys against * @returns The subset of the config */ function where(pattern: RegExp): Promise<Record<string, unknown>> { return send(NAME, WHERE, {pattern}, {expectReply: true, timeout: 10}) } /** * @description Subscribes to config updates * @param callback - The callback to call when config is updated * @returns The unsubscribe function */ function subscribe( callback: ( config: Record<string, unknown> | null, error: Error | null ) => void ): () => void { return subscriber(NAME, () => spawn(HANDLERS, NAME), callback) } /** * @description Clears the config */ export async function clearConfig(): Promise<void> { await send(NAME, CLEAR) } /** * @description Resets the config to a previous state * @param oldConfig - The previous config state * @returns The config object */ async function resetConfig(oldConfig: Record<string, unknown>) { return clearConfig().then(() => config(oldConfig)) } /** * @description Takes in flow.json or array of flow.json files and creates contract placeholders * @param data - The data to load * @param data.flowJSON - The flow.json or array of flow.json files * @param options - override flag * @param options.ignoreConflicts - ignore conflicts and override config */ async function load( data: { flowJSON: Record<string, unknown> | Record<string, unknown>[] }, { ignoreConflicts = false, }: { ignoreConflicts?: boolean } = {} ) { const network: string = await get("flow.network") const cleanedNetwork = cleanNetwork(network) const {flowJSON} = data invariant(Boolean(flowJSON), "config.load -- 'flowJSON' must be defined") invariant( !!cleanedNetwork, `Flow Network Required -- In order for FCL to load your contracts please define "flow.network" to "emulator", "local", "testnet", or "mainnet" in your config. See more here: https://developers.flow.com/tools/fcl-js/reference/configure-fcl` ) if (anyHasPrivateKeys(flowJSON)) { const isEmulator = cleanedNetwork === "emulator" logger.log({ title: "Private Keys Detected", message: `Private keys should be stored in a separate flow.json file for security. See more here: https://developers.flow.com/tools/flow-cli/security`, level: isEmulator ? logger.LEVELS.warn : logger.LEVELS.error, }) invariant( isEmulator, `Private keys should be stored in a separate flow.json file for security. See more here: https://developers.flow.com/tools/flow-cli/security` ) } for (const [key, value] of Object.entries( getContracts(flowJSON, cleanedNetwork) )) { const contractConfigKey = `0x${key}` const existingContractConfigKey = await get(contractConfigKey) if ( existingContractConfigKey && existingContractConfigKey !== value && !ignoreConflicts ) { logger.log({ title: "Contract Placeholder Conflict Detected", message: `A generated contract placeholder from config.load conflicts with a placeholder you've set manually in config have the same name.`, level: logger.LEVELS.warn, }) } else { put(contractConfigKey, value) } const systemContractConfigKey = `system.contracts.${key}` const systemExistingContractConfigKeyValue = await get( systemContractConfigKey ) if ( systemExistingContractConfigKeyValue && systemExistingContractConfigKeyValue !== value && !ignoreConflicts ) { logger.log({ title: "Contract Placeholder Conflict Detected", message: `A generated contract placeholder from config.load conflicts with a placeholder you've set manually in config have the same name.`, level: logger.LEVELS.warn, }) } else { put(systemContractConfigKey, value) } } } /** * @description Sets the config * @param values - The values to set * @returns The config object */ function config(values?: Record<string, unknown>) { if (values != null && typeof values === "object") { Object.keys(values).map(d => put(d, values[d])) } return { put, get, all, first, update, delete: _delete, where, subscribe, overload, load, } } config.put = put config.get = get config.all = all config.first = first config.update = update config.delete = _delete config.where = where config.subscribe = subscribe config.overload = overload config.load = load export {config} export {getContracts} from "./utils/utils" /** * @description Temporarily overloads the config with the given values and calls the callback * @param values - The values to overload the config with * @param callback - The callback to call with the overloaded config * @returns The result of the callback */ async function overload<T>( values: Record<string, unknown>, callback: (oldConfig: Record<string, unknown>) => T ) { const oldConfig = await all() try { config(values) const result = await callback(await all()) return result } finally { await resetConfig(oldConfig) } } ================================================ FILE: packages/config/src/utils/utils.test.ts ================================================ import {getContracts, anyHasPrivateKeys, FlowJson} from "./utils" describe("getContracts", () => { test("it should gather contract aliases for flow.json", () => { const flowJSON: FlowJson = { networks: { emulator: "127.0.0.1:3569", mainnet: "access.mainnet.nodes.onflow.org:9000", testnet: "access.devnet.nodes.onflow.org:9000", }, contracts: { HelloWorld: { source: "./cadence/contracts/HelloWorld.cdc", aliases: { emulator: "0x123", testnet: "0x124", mainnet: "0x125", }, }, FooBar: { source: "./cadence/contracts/FooBar.cdc", aliases: { emulator: "0x223", testnet: "0x224", mainnet: "0x225", }, }, }, dependencies: { FungibleToken: { source: "emulator://123.FungibleToken", hash: "123", aliases: { emulator: "0x333", testnet: "0x222", mainnet: "0x111", }, }, }, accounts: { "emulator-account": { address: "f8d6e0586b0a20c7", key: "ba68d45a5acaa52f3cacf4ad3a64d9523e0ce0ae3addb1ee6805385b380b7646", }, }, deployments: { emulator: { "emulator-account": ["HelloWorld"], }, }, } const emulatorMappings = { HelloWorld: "0x123", FooBar: "0x223", FungibleToken: "0x333", } const testnetMappings = { HelloWorld: "0x124", FooBar: "0x224", FungibleToken: "0x222", } const mainnetMappings = { HelloWorld: "0x125", FooBar: "0x225", FungibleToken: "0x111", } expect(getContracts(flowJSON, "emulator")).toEqual(emulatorMappings) expect(getContracts(flowJSON, "testnet")).toEqual(testnetMappings) expect(getContracts(flowJSON, "mainnet")).toEqual(mainnetMappings) // Also takes array expect(getContracts([flowJSON], "emulator")).toEqual(emulatorMappings) expect(getContracts([flowJSON], "testnet")).toEqual(testnetMappings) expect(getContracts([flowJSON], "mainnet")).toEqual(mainnetMappings) }) test("it should handle multiple contracts in deployments section", () => { const flowJSON: FlowJson = { accounts: { "emulator-account": { address: "f8d6e0586b0a20c7", }, "testnet-account": { address: "a470920a30c770a5", }, }, deployments: { emulator: { "emulator-account": ["ContractA", "ContractB", "ContractC"], }, testnet: { "testnet-account": ["ContractA", "ContractB"], }, }, } const emulatorMappings = { ContractA: "f8d6e0586b0a20c7", ContractB: "f8d6e0586b0a20c7", ContractC: "f8d6e0586b0a20c7", } const testnetMappings = { ContractA: "a470920a30c770a5", ContractB: "a470920a30c770a5", } expect(getContracts(flowJSON, "emulator")).toEqual(emulatorMappings) expect(getContracts(flowJSON, "testnet")).toEqual(testnetMappings) }) test("it should merge deployments with contract aliases", () => { const flowJSON: FlowJson = { contracts: { HelloWorld: { source: "./cadence/contracts/HelloWorld.cdc", aliases: { emulator: "0x123", }, }, }, accounts: { "emulator-account": { address: "0xf8d6e0586b0a20c7", }, }, deployments: { emulator: { "emulator-account": ["DeployedContract"], }, }, } const emulatorMappings = { HelloWorld: "0x123", DeployedContract: "0xf8d6e0586b0a20c7", } expect(getContracts(flowJSON, "emulator")).toEqual(emulatorMappings) }) test("it should handle multiple accounts with multiple contracts each", () => { const flowJSON: FlowJson = { accounts: { "account-1": { address: "0x01", }, "account-2": { address: "0x02", }, }, deployments: { emulator: { "account-1": ["Contract1A", "Contract1B"], "account-2": ["Contract2A", "Contract2B"], }, }, } const emulatorMappings = { Contract1A: "0x01", Contract1B: "0x01", Contract2A: "0x02", Contract2B: "0x02", } expect(getContracts(flowJSON, "emulator")).toEqual(emulatorMappings) }) test("it should handle fork networks that inherit from parent network", () => { const flowJSON: FlowJson = { networks: { emulator: "127.0.0.1:3569", mainnet: "access.mainnet.nodes.onflow.org:9000", "mainnet-fork": { host: "127.0.0.1:3569", fork: "mainnet", }, }, contracts: { FlowToken: { source: "./contracts/FlowToken.cdc", aliases: { emulator: "0x0ae53cb6e3f42a79", mainnet: "0x1654653399040a61", }, }, FungibleToken: { source: "./contracts/FungibleToken.cdc", aliases: { emulator: "0xee82856bf20e2aa6", mainnet: "0xf233dcee88fe0abe", }, }, }, } // Fork network should inherit mainnet aliases const mainnetForkMappings = { FlowToken: "0x1654653399040a61", FungibleToken: "0xf233dcee88fe0abe", } expect(getContracts(flowJSON, "mainnet-fork")).toEqual(mainnetForkMappings) // Regular mainnet should still work expect(getContracts(flowJSON, "mainnet")).toEqual(mainnetForkMappings) }) test("it should handle testnet fork networks", () => { const flowJSON: FlowJson = { networks: { testnet: "access.devnet.nodes.onflow.org:9000", "testnet-fork": { host: "127.0.0.1:3569", fork: "testnet", }, }, dependencies: { USDC: { source: "testnet://a983fecbed621163.USDC", hash: "abc123", aliases: { testnet: "0xa983fecbed621163", }, }, }, } const testnetForkMappings = { USDC: "0xa983fecbed621163", } expect(getContracts(flowJSON, "testnet-fork")).toEqual(testnetForkMappings) }) test("it should handle fork networks with explicit deployments", () => { const flowJSON: FlowJson = { networks: { mainnet: "access.mainnet.nodes.onflow.org:9000", "mainnet-fork": { host: "127.0.0.1:3569", fork: "mainnet", }, }, contracts: { FlowToken: { source: "./contracts/FlowToken.cdc", aliases: { mainnet: "0x1654653399040a61", }, }, }, accounts: { "flow-token-mainnet": { address: "0x1654653399040a61", }, }, deployments: { "mainnet-fork": { "flow-token-mainnet": ["FlowToken"], }, }, } // Deployments are explicit - uses mainnet-fork deployment (not inherited) const mainnetForkMappings = { FlowToken: "0x1654653399040a61", } expect(getContracts(flowJSON, "mainnet-fork")).toEqual(mainnetForkMappings) }) test("it should use fork-specific aliases over inherited aliases (fallback behavior)", () => { const flowJSON: FlowJson = { networks: { mainnet: "access.mainnet.nodes.onflow.org:9000", "mainnet-fork": { host: "127.0.0.1:3569", fork: "mainnet", }, }, contracts: { FlowToken: { source: "./contracts/FlowToken.cdc", aliases: { mainnet: "0x1654653399040a61", "mainnet-fork": "0x0000000000000001", // Override for fork! }, }, FungibleToken: { source: "./contracts/FungibleToken.cdc", aliases: { mainnet: "0xf233dcee88fe0abe", // No mainnet-fork alias - should inherit from mainnet }, }, }, } const mainnetForkMappings = { FlowToken: "0x0000000000000001", // Uses fork-specific alias FungibleToken: "0xf233dcee88fe0abe", // Falls back to mainnet alias } expect(getContracts(flowJSON, "mainnet-fork")).toEqual(mainnetForkMappings) }) }) describe("anyHasPrivateKeys", () => { test("it should return true if private keys exist in account", () => { const flowJSON = { accounts: { "emulator-account": { address: "f8d6e0586b0a20c7", key: "ba68d45a5acaa52f3cacf4ad3a64d9523e0ce0ae3addb1ee6805385b380b7646", }, }, } const flowJSONTwo = { accounts: { "testnet-account": { address: "f8d6e0586b0a20c7", key: "ba68d45a5acaa52f3cacf4ad3a64d9523e0ce0ae3addb1ee6805385b380b7646", }, }, } const flowJSONThree = { accounts: { "emulator-account": { address: "f8d6e0586b0a20c7", }, }, } expect(anyHasPrivateKeys(flowJSON)).toBe(true) expect(anyHasPrivateKeys([flowJSON, flowJSONTwo])).toBe(true) expect(anyHasPrivateKeys([flowJSON, flowJSONTwo, flowJSONThree])).toBe(true) }) test("should return false for environmental variables", async () => { const flowJSON = { accounts: { "emulator-account": { address: "f8d6e0586b0a20c7", key: "$FLOW_EMULATOR_PRIVATE_KEY", }, }, } expect(anyHasPrivateKeys(flowJSON)).toBe(false) }) test("it should return false if no private keys exist in account", () => { const flowJSON = { accounts: { "emulator-account": { address: "f8d6e0586b0a20c7", }, }, } const flowJSONTwo = { accounts: { "emulator-account": { address: "f8d6e0586b0a20c7", }, }, } expect(anyHasPrivateKeys(flowJSON)).toBe(false) expect(anyHasPrivateKeys([flowJSON, flowJSONTwo])).toBe(false) }) test("it should return false if private key value is in separate file", () => { const flowJSON = { accounts: { "emulator-account": { address: "f8d6e0586b0a20c7", key: { type: "file", location: "./emulator.key", }, }, }, } const flowJSONTwo = { accounts: { "emulator-account": { address: "f8d6e0586b0a20c7", }, }, } const flowJSONThree = { accounts: { "emulator-account": { address: "f8d6e0586b0a20c7", key: "ba68d45a5acaa52f3cacf4ad3a64d9523e0ce0ae3addb1ee6805385b380b7646", }, }, } expect(anyHasPrivateKeys(flowJSON)).toBe(false) expect(anyHasPrivateKeys([flowJSON, flowJSONTwo])).toBe(false) expect(anyHasPrivateKeys(flowJSONThree)).toBe(true) }) }) ================================================ FILE: packages/config/src/utils/utils.ts ================================================ /** * Fork network configuration * Allows a custom network to inherit contract aliases from a parent network */ export interface ForkNetworkConfig { host: string fork: string } /** * Network configuration - either a string (host) or fork configuration */ export type NetworkConfig = string | ForkNetworkConfig export interface FlowJson { networks?: { [key: string]: NetworkConfig } accounts?: { [key: string]: { address: string key?: string | object } } contracts?: { [key: string]: { source: string aliases: { [key: string]: string } // Optional canonical field for import aliases // When set, indicates this contract is an alias of another contract // Example: FUSD1 has canonical="FUSD" means FUSD1 is an alias of FUSD // Results in: import FUSD as FUSD1 from 0x... canonical?: string } } dependencies?: { [key: string]: { source: string hash: string aliases: { [key: string]: string } // Optional canonical field for import aliases // When set, indicates this dependency is an alias of another contract // Example: FungibleTokenV2 has canonical="FungibleToken" // Results in: import FungibleToken as FungibleTokenV2 from 0x... canonical?: string } } deployments?: { [key: string]: { [contract: string]: string[] } } } const pipe = (...funcs: ((v: any) => any)[]) => (v: any) => { return funcs.reduce((res, func) => { return func(res) }, v) } /*** * Merge multiple functions returning objects into one object. * @param funcs - Functions to merge * @return Merged object */ const mergePipe = (...funcs: ((v: any) => any)[]) => (v: any) => { return funcs.reduce((res, func) => { return {...res, ...func(v)} }, {}) } /** * @description Object check * @param value - Value to check * @returns Is object status */ const isObject = <T>(value: T): boolean => value && typeof value === "object" && !Array.isArray(value) /** * @description Deep merge multiple objects. * @param target - Target object * @param sources - Source objects * @returns Merged object */ const mergeDeep = (target: any, ...sources: any[]): any => { if (!sources.length) return target const source = sources.shift() if (isObject(target) && isObject(source)) { for (const key in source) { if (isObject(source[key])) { if (!target[key]) Object.assign(target, {[key]: {}}) mergeDeep(target[key], source[key]) } else { Object.assign(target, {[key]: source[key]}) } } } return mergeDeep(target, ...sources) } /** * @description Support if/then/else behavior in a function way. * @param testFn - Function to test * @param posCond - Function to run if testFn is true * @param negCond - Function to run it testFn is false * @returns Function that returns the result of posCond or negCond */ export const ifElse = <T, U>(testFn: (v: T) => U, posCond: (v: T) => U, negCond: (v: T) => U) => (v: T) => testFn(v) ? posCond(v) : negCond(v) /** * @description Deep merge multiple Flow JSON. * @param value - Flow JSON or array of Flow JSONs * @returns Merged Flow JSON */ const mergeFlowJSONs = (value: FlowJson | FlowJson[]) => Array.isArray(value) ? mergeDeep({}, ...value) : value /** * @description Filter out contracts section of flow.json. * @param obj - Flow JSON * @returns Contracts section of Flow JSON */ const filterContracts = (obj: FlowJson) => (obj.contracts ? obj.contracts : {}) /** * @description Filter out dependencies section of flow.json. * @param obj - Flow JSON * @returns Dependencies section of Flow JSON */ const filterDependencies = (obj: FlowJson) => obj.dependencies ? obj.dependencies : {} /** * @description Gathers contract addresses by network * @param network - Network to gather addresses for * @param fallbackNetwork - Optional fallback network for fork networks * @returns Contract names by addresses mapping e.g { "HelloWorld": "0x123" } */ const mapContractAliasesToNetworkAddress = (network: string, fallbackNetwork?: string) => (contracts: Record<string, any>) => { return Object.entries(contracts).reduce( (c, [key, value]) => { const alias = value?.aliases?.[network] || (fallbackNetwork && value?.aliases?.[fallbackNetwork]) if (alias) { c[key] = alias } return c }, {} as Record<string, string> ) } /** * @description Gathers dependency addresses by network * @param network - Network to gather addresses for * @param fallbackNetwork - Optional fallback network for fork networks * @returns Dependency names by addresses mapping e.g { "HelloWorld": "0x123" } */ const mapDependencyAliasesToNetworkAddress = (network: string, fallbackNetwork?: string) => (dependencies: Record<string, any>) => { return Object.entries(dependencies).reduce( (c, [key, value]) => { const alias = value?.aliases?.[network] || (fallbackNetwork && value?.aliases?.[fallbackNetwork]) if (alias) { c[key] = alias } return c }, {} as Record<string, string> ) } /** * @description Gathers contract canonical references for import aliases * @param contracts - Contracts from flow.json * @returns Contract canonical names mapping e.g { "FUSD1.canonical": "FUSD" } * * Import aliases allow multiple deployments of the same contract to different addresses. * The canonical field specifies the actual contract name in the source code. * * Example: * - Input: { FUSD1: { canonical: "FUSD" } } * - Output: { "FUSD1.canonical": "FUSD" } * - Config stored as: system.contracts.FUSD1.canonical = "FUSD" * - Resolves to: import FUSD as FUSD1 from 0x... */ const mapContractCanonicals = (contracts: Record<string, any>) => { return Object.entries(contracts).reduce( (c, [key, value]) => { if (value?.canonical) { c[`${key}.canonical`] = value.canonical } return c }, {} as Record<string, string> ) } /** * @description Gathers dependency canonical references for import aliases * @param dependencies - Dependencies from flow.json * @returns Dependency canonical names mapping e.g { "FungibleTokenV2.canonical": "FungibleToken" } * * Import aliases allow multiple deployments of the same contract to different addresses. * The canonical field specifies the actual contract name in the source code. * * Example: * - Input: { FungibleTokenV2: { canonical: "FungibleToken" } } * - Output: { "FungibleTokenV2.canonical": "FungibleToken" } * - Config stored as: system.contracts.FungibleTokenV2.canonical = "FungibleToken" * - Resolves to: import FungibleToken as FungibleTokenV2 from 0x... */ const mapDependencyCanonicals = (dependencies: Record<string, any>) => { return Object.entries(dependencies).reduce( (c, [key, value]) => { if (value?.canonical) { c[`${key}.canonical`] = value.canonical } return c }, {} as Record<string, string> ) } const mapDeploymentsToNetworkAddress = (network: string) => ({ deployments = {}, accounts = {}, }: Pick<FlowJson, "deployments" | "accounts">) => { const networkDeployment = deployments?.[network] if (!networkDeployment) return {} return Object.entries(networkDeployment).reduce((c, [key, value]) => { // Resolve account address const accountAddress = accounts[key]?.address if (!accountAddress) return c // Create an object assigning the address to the contract name. return value.reduce((acc, contract) => { return {...acc, [contract]: accountAddress} }, c) }, {}) } /** * @description Take in flow.json files and return contract to address mapping by network * @param jsons - Flow JSON or array of Flow JSONs * @param network - Network to gather addresses for (can be a fork network) * @returns Contract names by addresses mapping including canonical references * * Returns both contract addresses and canonical references for import aliases: * - Contract addresses: { "HelloWorld": "0x123", "FUSD1": "0xe223..." } * - Canonical references: { "FUSD1.canonical": "FUSD" } * * These are stored in config as: * - system.contracts.FUSD1 = "0xe223..." * - system.contracts.FUSD1.canonical = "FUSD" * * The canonical reference enables import alias resolution: * - import "FUSD1" → import FUSD as FUSD1 from 0xe223... * * Fork networks use fallback resolution: * - First collects all aliases from the parent network (e.g., "mainnet") * - Then overlays aliases from the fork network (e.g., "mainnet-fork") * - This allows fork-specific overrides while inheriting the rest */ export const getContracts = (jsons: FlowJson | FlowJson[], network: string) => { const mergedJson = mergeFlowJSONs(jsons) // Determine fallback network for fork networks const networkConfig = mergedJson.networks?.[network] const fallbackNetwork = networkConfig && typeof networkConfig === "object" && "fork" in networkConfig ? networkConfig.fork : undefined return mergePipe( mapDeploymentsToNetworkAddress(network), pipe( filterContracts, mapContractAliasesToNetworkAddress(network, fallbackNetwork) ), pipe( filterDependencies, mapDependencyAliasesToNetworkAddress(network, fallbackNetwork) ), pipe(filterContracts, mapContractCanonicals), pipe(filterDependencies, mapDependencyCanonicals) )(mergedJson) } /** * @description Checks if string is hexidecimal * @param str - String to check * @returns Is hexidecimal status */ const isHexidecimal = (str: unknown) => { // Check that it is a string if (typeof str !== "string") return false return /^[0-9A-Fa-f]+$/.test(str) } /** * @description Checks flow.json file for private keys * @param flowJSON - Flow JSON * @returns Has private keys status */ const hasPrivateKeys = (flowJSON: FlowJson) => { return Object.entries(flowJSON?.accounts ?? []).reduce( (hasPrivateKey, [, value]) => { if (hasPrivateKey) return true return ( value && Object.prototype.hasOwnProperty.call(value, "key") && isHexidecimal(value?.key) ) }, false ) } /** * @description Take in flow.json or array of flow.json files and checks for private keys * @param value - Flow JSON or array of Flow JSONs * @returns Has private keys status */ export const anyHasPrivateKeys = (value: FlowJson | FlowJson[]) => { if (Array.isArray(value)) return value.some(hasPrivateKeys) return hasPrivateKeys(value) } /** * @description Format network name, converting 'local' to 'emulator' * @param network - Network to format * @returns Formatted network name * * This function now accepts any network name (including custom fork networks). * It only transforms "local" to "emulator" for backward compatibility. */ export const cleanNetwork = (network: string): string => { return network?.toLowerCase() === "local" ? "emulator" : network?.toLowerCase() } ================================================ FILE: packages/config/tsconfig.json ================================================ { "extends": "../../tsconfig", // Change this to match your project "include": ["src/**/*"], "compilerOptions": { "declarationDir": "types", "rootDir": "src" } } ================================================ FILE: packages/demo/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: packages/demo/CHANGELOG.md ================================================ # @onflow/demo ## 1.9.0 ### Minor Changes - [#2745](https://github.com/onflow/fcl-js/pull/2745) [`706e08247a1e13eb49f01671772e2172bf664338`](https://github.com/onflow/fcl-js/commit/706e08247a1e13eb49f01671772e2172bf664338) Thanks [@mfbz](https://github.com/mfbz)! - Added react-native-sdk package, similar to react-sdk but for react-native applications. It fully supports all the same hooks available in react-sdk, plus the connect and profile components. It leverages fcl-react-native for managing blockchain interactions and it's compatible to both react-native and expo applications. ## 1.8.1 ### Patch Changes - [#2767](https://github.com/onflow/fcl-js/pull/2767) [`9a982e62e45a2f6f75fb566c0ce53ee56d3c447c`](https://github.com/onflow/fcl-js/commit/9a982e62e45a2f6f75fb566c0ce53ee56d3c447c) Thanks [@mfbz](https://github.com/mfbz)! - Trigger Changeset Release workflow. ## 1.8.0 ### Minor Changes - [#2765](https://github.com/onflow/fcl-js/pull/2765) [`0fe606e92ab3103dd29ba6a60c407c438aa86d9e`](https://github.com/onflow/fcl-js/commit/0fe606e92ab3103dd29ba6a60c407c438aa86d9e) Thanks [@mfbz](https://github.com/mfbz)! - Trigger Changeset Release workflow. ## 1.7.0 ### Minor Changes - [#2763](https://github.com/onflow/fcl-js/pull/2763) [`61a68051a6d6732c84a918b298eec5aabd561da7`](https://github.com/onflow/fcl-js/commit/61a68051a6d6732c84a918b298eec5aabd561da7) Thanks [@mfbz](https://github.com/mfbz)! - Trigger Changeset Release workflow. ## 1.6.0 ### Minor Changes - [#2761](https://github.com/onflow/fcl-js/pull/2761) [`aa737c359559d9f9f44d518805d5b1228aad9754`](https://github.com/onflow/fcl-js/commit/aa737c359559d9f9f44d518805d5b1228aad9754) Thanks [@mfbz](https://github.com/mfbz)! - Trigger Changeset Release workflow. ## 1.5.0 ### Minor Changes - [#2759](https://github.com/onflow/fcl-js/pull/2759) [`efad6e114bd53e11347c261aaa0e826a6452fd69`](https://github.com/onflow/fcl-js/commit/efad6e114bd53e11347c261aaa0e826a6452fd69) Thanks [@mfbz](https://github.com/mfbz)! - Trigger Changeset Release workflow. ## 1.4.0 ### Minor Changes - [#2756](https://github.com/onflow/fcl-js/pull/2756) [`60f568702df22f874cea5a756962f701e79783d6`](https://github.com/onflow/fcl-js/commit/60f568702df22f874cea5a756962f701e79783d6) Thanks [@mfbz](https://github.com/mfbz)! - Packages re-released after lerna update. ## 1.3.0 ### Minor Changes - [#2733](https://github.com/onflow/fcl-js/pull/2733) [`b5da56b93755f2871135573bfa806a8e12a50ddd`](https://github.com/onflow/fcl-js/commit/b5da56b93755f2871135573bfa806a8e12a50ddd) Thanks [@mfbz](https://github.com/mfbz)! - Add react-core package with platform-agnostic hooks with common utils and types extracted from react-sdk. Refactored react-sdk to use react-core package. ## 1.2.0 ### Minor Changes - [#2677](https://github.com/onflow/fcl-js/pull/2677) [`58f953381ee0826bfeb6d068c28bf8853b470915`](https://github.com/onflow/fcl-js/commit/58f953381ee0826bfeb6d068c28bf8853b470915) Thanks [@mfbz](https://github.com/mfbz)! - Added `NftCard` component for displaying NFT metadata. The component automatically fetches and renders NFT information including image, name, description, collection details, traits, and external links. Features loading states, error handling, dark mode support and optional display of traits and additional metadata through `showTraits` and `showExtra` props. - [#2685](https://github.com/onflow/fcl-js/pull/2685) [`f47dd3bd1efce2498b3ab731ca08da2326604f5f`](https://github.com/onflow/fcl-js/commit/f47dd3bd1efce2498b3ab731ca08da2326604f5f) Thanks [@mfbz](https://github.com/mfbz)! - Added standalone Profile component for displaying wallet information. The Profile component has been extracted from the Connect component modal to provide a reusable profile display that can be used independently. The component automatically detects connection state, showing a compact "No connected wallet" message when disconnected and full profile information when connected (including address, balance with cross-VM support, multi-token selector, copy/disconnect actions, and optional scheduled transactions). - [#2679](https://github.com/onflow/fcl-js/pull/2679) [`8ef9cc6933f61c45e32a69c9d723ab669a7757ec`](https://github.com/onflow/fcl-js/commit/8ef9cc6933f61c45e32a69c9d723ab669a7757ec) Thanks [@mfbz](https://github.com/mfbz)! - Added `ScheduledTransactionList` component, a scrollable list that displays scheduled transactions for a Flow account with support for MetadataViews.Display (thumbnails, names, descriptions), transaction cancellation, automatic refresh, responsive design and dark mode. Each card shows the scheduled execution time, fee, priority, and effort with an optional cancel button for pending transactions. Enhanced `Connect` component to display scheduled transactions in the profile modal. The modal now shows the user's scheduled transactions below their account info with a configurable `modalConfig` prop to control visibility. - [#2673](https://github.com/onflow/fcl-js/pull/2673) [`e4e921a552caa78a8ef02b50a3e8431faee6764a`](https://github.com/onflow/fcl-js/commit/e4e921a552caa78a8ef02b50a3e8431faee6764a) Thanks [@mfbz](https://github.com/mfbz)! - Added flow-react-sdk-starter banner. - [#2681](https://github.com/onflow/fcl-js/pull/2681) [`3d6d1ab75898ff3b2d151ee2d1bd5923461f8b51`](https://github.com/onflow/fcl-js/commit/3d6d1ab75898ff3b2d151ee2d1bd5923461f8b51) Thanks [@mfbz](https://github.com/mfbz)! - Optimized hot reload for demo running locally ## 1.1.0 ### Minor Changes - [#2592](https://github.com/onflow/fcl-js/pull/2592) [`52688c33eb41862a5daa4226e2fdfa98afab79a4`](https://github.com/onflow/fcl-js/commit/52688c33eb41862a5daa4226e2fdfa98afab79a4) Thanks [@mfbz](https://github.com/mfbz)! - Added `useCrossVmBridgeTokenFromEvm` hook for bridging fungible tokens from Flow EVM to Cadence. This hook withdraws tokens from the signer's Cadence-Owned Account (COA) in EVM and deposits them into their Cadence vault, automatically configuring the vault if needed. - [#2663](https://github.com/onflow/fcl-js/pull/2663) [`8ea4051b2a80024ade40bd58227c81f476011299`](https://github.com/onflow/fcl-js/commit/8ea4051b2a80024ade40bd58227c81f476011299) Thanks [@mfbz](https://github.com/mfbz)! - Added improved Google Analytics tracking for hash anchors - [#2624](https://github.com/onflow/fcl-js/pull/2624) [`7963d7aa2984e4fd0ad94182a65c8c4065f1d98c`](https://github.com/onflow/fcl-js/commit/7963d7aa2984e4fd0ad94182a65c8c4065f1d98c) Thanks [@mfbz](https://github.com/mfbz)! - Added `useCrossVmBridgeNftFromEvm` hook for bridging NFTs from Flow EVM to Cadence. This hook withdraws an NFT from the signer's Cadence-Owned Account (COA) in EVM and deposits it into their Cadence collection, automatically configuring the collection if needed. - [#2661](https://github.com/onflow/fcl-js/pull/2661) [`e651d625af8e516d935e74d885524eca741dd9e6`](https://github.com/onflow/fcl-js/commit/e651d625af8e516d935e74d885524eca741dd9e6) Thanks [@mfbz](https://github.com/mfbz)! - Add `useFlowNftMetadata` hook to fetch NFT metadata including name, description, thumbnail, traits, and collection information from Flow blockchain accounts. - [#2657](https://github.com/onflow/fcl-js/pull/2657) [`5a0e093c5078253266b90cfc01884f532e0bd41e`](https://github.com/onflow/fcl-js/commit/5a0e093c5078253266b90cfc01884f532e0bd41e) Thanks [@mfbz](https://github.com/mfbz)! - Updated playground adding anchoring support and full responsive mode for mobile and tablet - [#2662](https://github.com/onflow/fcl-js/pull/2662) [`5e88787e5b387bdc7289ff84b8d3e74f355b9ea8`](https://github.com/onflow/fcl-js/commit/5e88787e5b387bdc7289ff84b8d3e74f355b9ea8) Thanks [@mfbz](https://github.com/mfbz)! - Added Google Analytics and SEO improvements ================================================ FILE: packages/demo/DEPLOY.md ================================================ # Playground Deployment Workflow ## How It Works The `playground` branch always points to the latest release tag and is deployed to Vercel. **Key principle:** Playground = released version. ## Automatic Sync ### When - Automatically on every package release (e.g., `@onflow/fcl@1.2.3`) - Triggered by changesets when tags are created ### What happens 1. Workflow detects new release tag 2. Force-updates playground branch to point at the tag 3. Vercel deploys the tagged release ## Emergency Hotfixes If demo needs urgent fix before next release: ### Option 1: Patch Release Create a patch release for demo package only with changeset. ### Option 2: Manual Fix ```bash git checkout playground # Fix demo files only (packages/demo/**) git commit -m "fix(demo): emergency hotfix" git push origin playground ``` **Warning:** Next release will overwrite playground. Emergency fix MUST go in next release! ## Rules ### DO - Let the workflow handle normal releases - Create patch releases for important fixes - Only manually fix demo files (`packages/demo/**`) - Ensure emergency fixes go in next release ### DON'T - Don't let emergency fixes skip the next release - Don't use manual fixes for non-critical issues ================================================ FILE: packages/demo/README.md ================================================ # Demo A lightweight internal demo project for testing FCL and Kit packages during development. ## Purpose This demo application is designed to: - Test FCL and Kit packages in a real React environment - Provide hot reload capabilities for fast development iteration - Serve as a simple sandbox for testing new features - Demonstrate integration patterns between FCL and Kit - Test contract deployment and interaction with Flow emulator ## Prerequisites Before you begin, ensure you have: - **Node.js** (v14+ recommended) - **Flow CLI** installed ([Installation Guide](https://developers.flow.com/tools/flow-cli/install)) - **npm** or **yarn** package manager Verify Flow CLI installation: ```bash flow version ``` ## Quick Start From the root of the repository: ```bash # Install dependencies (installs all workspace packages) npm install # Build packages (required for demo to work) npm run build # Start the demo npm run demo ``` ## Flow Emulator & Dev Wallet Setup ### 1. Start Flow Emulator The Flow emulator simulates the Flow blockchain locally for development and testing. ```bash # Start the emulator (runs on http://127.0.0.1:3569) flow emulator ``` **Keep this running in a separate terminal** - the emulator needs to stay active for contract deployment and testing. ### 2. Start Dev Wallet The dev wallet provides a simple wallet interface for testing on the emulator: ```bash # Start the dev wallet (runs on http://localhost:8701) flow dev-wallet ``` **Keep this running in another terminal** - you'll need it for authentication and transaction signing. ### 3. Start the Demo App ```bash # From the demo directory npm run dev ``` The app will be available at `http://localhost:5173` ## Development Environments ### Local Development (Emulator) ```bash # Terminal 1: Start emulator flow emulator # Terminal 2: Start dev wallet flow dev-wallet # Terminal 3: Start app cd packages/demo npm run dev ``` **Configuration**: Uses `emulator-account` (address: `f8d6e0586b0a20c7`) ### Testnet Development ```bash # Run against deployed contracts on Flow Testnet npm run dev:testnet ``` **Note**: Requires Flow Wallet. ## Features Provided - `@onflow/react-sdk` setup and configuration - Wallet Discovery (including Dev Wallet on Emulator) - CLI private key separation for security - Flow.json loading for contract placeholders - Authentication - CDC file loader - Custom hooks - Interaction examples ## What You Can Test ### Authentication - Connect/disconnect wallet - View user profile information - Test authentication state management ### Contract Interaction - **Execute scripts** to read contract state - **Send transactions** to modify contract state ### Queries - Execute Cadence scripts - Test data fetching and caching - Debug query responses ### Transactions - Send transactions to the blockchain - Monitor transaction status - Test error handling - Sign transactions with dev wallet ## Package Dependencies This demo uses workspace references to: - `@onflow/fcl` - Core Flow Client Library - `@onflow/react-sdk` - React hooks for Flow - `@onflow/typedefs` - Type definitions Changes to these packages are immediately available via workspace symlinks. ================================================ FILE: packages/demo/emulator.key ================================================ ba68d45a5acaa52f3cacf4ad3a64d9523e0ce0ae3addb1ee6805385b380b7646 ================================================ FILE: packages/demo/eslint.config.ts ================================================ import js from "@eslint/js" import globals from "globals" import reactHooks from "eslint-plugin-react-hooks" import reactRefresh from "eslint-plugin-react-refresh" import tseslint, {Config} from "typescript-eslint" const config: Config = tseslint.config( {ignores: ["dist"]}, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ["**/*.{ts,tsx}"], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { "react-hooks": reactHooks, "react-refresh": reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, "react-refresh/only-export-components": [ "warn", {allowConstantExport: true}, ], "@typescript-eslint/no-unused-vars": "off", "no-unused-vars": "off", }, } ) export default config ================================================ FILE: packages/demo/index.html ================================================ <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/png" href="/assets/icon.png" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="theme-color" content="#00EF8B" /> <title>Flow React SDK - Playground for React Hooks & Components
================================================ FILE: packages/demo/package.json ================================================ { "name": "@onflow/demo", "private": true, "version": "1.9.0", "type": "module", "scripts": { "dev": "cross-env VITE_FLOW_NETWORK=emulator vite", "dev-wait": "npm run dev & wait-on tcp:8888 tcp:8701 tcp:3000", "dev:emulator": "concurrently \"npm run flow:emulator\" \"npm run flow:dev-wallet\" \"npm run dev-wait\"", "dev:testnet": "concurrently \"npm run watch:react-sdk\" \"cross-env VITE_FLOW_NETWORK=testnet vite\"", "dev:mainnet": "concurrently \"npm run watch:react-sdk\" \"cross-env VITE_FLOW_NETWORK=mainnet vite\"", "watch:react-sdk": "cd ../react-sdk && npm run start", "flow:emulator": "flow emulator", "flow:dev-wallet": "flow dev-wallet", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview" }, "dependencies": { "@onflow/fcl": "file:../fcl", "@onflow/react-sdk": "file:../react-sdk", "@onflow/typedefs": "file:../typedefs", "@react-aria/utils": "^3.25.3", "@react-stately/flags": "^3.0.4", "@react-stately/utils": "^3.10.4", "@types/react-syntax-highlighter": "^15.5.13", "react": "^19.0.0", "react-dom": "^19.0.0", "react-syntax-highlighter": "^15.6.6" }, "devDependencies": { "@eslint/js": "^9.25.0", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.4.1", "autoprefixer": "^10.4.21", "concurrently": "^8.0.1", "cross-env": "^7.0.3", "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.17", "typescript": "^5.6.3", "typescript-eslint": "^8.30.1", "vite": "^6.3.5", "wait-on": "^7.0.1" }, "optionalDependencies": { "@esbuild/linux-x64": "^0.25.8" }, "alias": { "@onflow/fcl": "../fcl/src/fcl.ts", "@onflow/react-sdk": "../react-sdk/src/index.ts" } } ================================================ FILE: packages/demo/postcss.config.js ================================================ import tailwindcss from "tailwindcss" import tailwindConfig from "./tailwind.config.js" import autoprefixer from "autoprefixer" export default { plugins: [tailwindcss(tailwindConfig), autoprefixer], } ================================================ FILE: packages/demo/public/robots.txt ================================================ # Allow all crawlers User-agent: * Allow: / # Sitemap Sitemap: https://react.flow.com/sitemap.xml ================================================ FILE: packages/demo/public/sitemap.xml ================================================ https://react.flow.com/ weekly 1.0 ================================================ FILE: packages/demo/src/app.css ================================================ @import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap"); @tailwind base; @tailwind components; @tailwind utilities; @layer utilities { .scrollbar-hide { -ms-overflow-style: none !important; /* IE and Edge */ scrollbar-width: none !important; /* Firefox */ } .scrollbar-hide::-webkit-scrollbar { display: none !important; /* Chrome, Safari and Opera */ } /* Also hide scrollbar for all children */ .scrollbar-hide * { -ms-overflow-style: none !important; scrollbar-width: none !important; } .scrollbar-hide *::-webkit-scrollbar { display: none !important; } .text-balance { text-wrap: balance; } } @layer base { html { font-family: "Inter", system-ui, sans-serif; scroll-behavior: smooth; scroll-padding-top: 6rem; /* Account for sticky header */ } body { margin: 0; padding: 0; overflow-x: hidden; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } /* Smooth transitions for all interactive elements */ * { transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; } } @layer components { .flow-gradient-text { @apply from-flow-primary via-flow-500 to-flow-600 bg-gradient-to-r bg-clip-text text-transparent; } .flow-glow { box-shadow: 0 0 20px rgba(0, 239, 139, 0.3); } .dark .flow-glow { box-shadow: 0 0 30px rgba(0, 239, 139, 0.5); } .sketch-border { position: relative; } .sketch-border::before { content: ""; position: absolute; top: -1px; left: -1px; right: -1px; bottom: -1px; background: linear-gradient( 45deg, transparent, rgba(0, 239, 139, 0.1), transparent ); border-radius: inherit; z-index: -1; } } /* Custom scrollbar for webkit browsers */ ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { @apply bg-gray-100 dark:bg-gray-800; } ::-webkit-scrollbar-thumb { @apply rounded-full bg-gray-300 hover:bg-gray-400 dark:bg-gray-600 dark:hover:bg-gray-500; } /* Selection colors */ ::selection { @apply bg-flow-primary/20 text-flow-900; } .dark ::selection { @apply bg-flow-primary/30 text-flow-100; } ================================================ FILE: packages/demo/src/app.tsx ================================================ import {useEffect, useState} from "react" import "./app.css" import {ContentSection} from "./components/content-section" import {ContentSidebar} from "./components/content-sidebar" import FlowProviderWrapper from "./components/flow-provider-wrapper" import {Footer} from "./components/footer" import {Header} from "./components/header" function AppContent() { // Move dark mode state outside FlowProvider to prevent sidebar reset on network change const [darkMode, setDarkMode] = useState(() => { if (typeof window !== "undefined") { const saved = localStorage.getItem("darkMode") return saved ? JSON.parse(saved) : false } return false }) const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false) const toggleDarkMode = () => { setDarkMode((prev: boolean) => { const newValue = !prev if (typeof window !== "undefined") { localStorage.setItem("darkMode", JSON.stringify(newValue)) } return newValue }) } // Prevent body scroll when mobile sidebar is open useEffect(() => { if (mobileSidebarOpen) { document.body.style.overflow = "hidden" } else { document.body.style.overflow = "unset" } return () => { document.body.style.overflow = "unset" } }, [mobileSidebarOpen]) return (
Skip to main content
setMobileSidebarOpen(!mobileSidebarOpen)} mobileSidebarOpen={mobileSidebarOpen} />
{mobileSidebarOpen && (
setMobileSidebarOpen(false)} aria-hidden="true" /> )}

Menu

setMobileSidebarOpen(false)} />
) } export function App() { return } ================================================ FILE: packages/demo/src/components/advanced-cards/dark-mode-card.tsx ================================================ import {DemoCard} from "../ui/demo-card" import {useDarkMode} from "../flow-provider-wrapper" import {PlusGridIcon} from "../ui/plus-grid" import {useState} from "react" const DARK_MODE_CODE = `import { FlowProvider, useDarkMode } from "@onflow/react-sdk" import { useState } from "react" function App() { const [isDark, setIsDark] = useState(false) return ( ) } function DarkModeToggle({ onToggle }) { const { isDark } = useDarkMode() return ( ) }` export function DarkModeCard() { const {darkMode, toggleDarkMode} = useDarkMode() const [demoMode, setDemoMode] = useState(darkMode) const handleToggle = () => { const newMode = !demoMode setDemoMode(newMode) // Simulate the actual dark mode toggle for demo purposes toggleDarkMode() } return (

Dark Mode Demo

{darkMode ? "🌙" : "☀️"}

Current Mode: {darkMode ? "Dark" : "Light"}

Click the toggle to switch themes

) } ================================================ FILE: packages/demo/src/components/advanced-cards/theming-card.tsx ================================================ import {DemoCard} from "../ui/demo-card" import {useDarkMode} from "../flow-provider-wrapper" import {PlusGridIcon} from "../ui/plus-grid" import {useState} from "react" import {FlowProvider} from "@onflow/react-sdk" import {Connect, TransactionButton} from "@onflow/react-sdk" const THEMING_CODE = `import { FlowProvider } from "@onflow/react-sdk" const customTheme = { colors: { primary: "flow-bg-purple-600 dark:flow-bg-purple-400", primaryForeground: "flow-text-white dark:flow-text-white", secondary: "flow-bg-purple-100 dark:flow-bg-purple-900", secondaryForeground: "flow-text-purple-900 dark:flow-text-purple-100", accent: "flow-bg-purple-700 dark:flow-bg-purple-300", background: "flow-bg-white dark:flow-bg-purple-950", foreground: "flow-text-purple-900 dark:flow-text-purple-100", muted: "flow-bg-purple-50 dark:flow-bg-purple-800", mutedForeground: "flow-text-purple-500 dark:flow-text-purple-400", border: "flow-border-purple-200 dark:flow-border-purple-700", } } function App() { return ( ) }` export function ThemingCard() { const {darkMode} = useDarkMode() const [selectedTheme, setSelectedTheme] = useState("default") const themes = { default: { name: "Flow Default", theme: null, colors: { primary: darkMode ? "bg-black" : "bg-gray-900", secondary: darkMode ? "bg-gray-700" : "bg-gray-200", accent: "bg-flow-primary", }, }, purple: { name: "Purple Theme", theme: { colors: { primary: "flow-bg-purple-600 dark:flow-bg-purple-400", primaryForeground: "flow-text-white", secondary: "flow-bg-purple-100 dark:flow-bg-purple-900", secondaryForeground: "flow-text-purple-900 dark:flow-text-purple-100", accent: "flow-bg-purple-700 dark:flow-bg-purple-300", background: "flow-bg-white dark:flow-bg-purple-950", foreground: "flow-text-purple-900 dark:flow-text-purple-100", muted: "flow-bg-purple-50 dark:flow-bg-purple-800", mutedForeground: "flow-text-purple-500 dark:flow-text-purple-400", border: "flow-border-purple-200 dark:flow-border-purple-700", link: "flow-text-purple-600 dark:flow-text-purple-300", }, }, colors: { primary: darkMode ? "bg-purple-400" : "bg-purple-600", secondary: darkMode ? "bg-purple-800" : "bg-purple-100", accent: darkMode ? "bg-purple-300" : "bg-purple-700", }, }, emerald: { name: "Emerald Theme", theme: { colors: { primary: "flow-bg-emerald-600 dark:flow-bg-emerald-400", primaryForeground: "flow-text-white", secondary: "flow-bg-emerald-100 dark:flow-bg-emerald-900", secondaryForeground: "flow-text-emerald-900 dark:flow-text-emerald-100", accent: "flow-bg-emerald-700 dark:flow-bg-emerald-300", background: "flow-bg-white dark:flow-bg-emerald-950", foreground: "flow-text-emerald-900 dark:flow-text-emerald-100", muted: "flow-bg-emerald-50 dark:flow-bg-emerald-800", mutedForeground: "flow-text-emerald-500 dark:flow-text-emerald-400", border: "flow-border-emerald-200 dark:flow-border-emerald-700", link: "flow-text-emerald-600 dark:flow-text-emerald-300", }, }, colors: { primary: darkMode ? "bg-emerald-400" : "bg-emerald-600", secondary: darkMode ? "bg-emerald-800" : "bg-emerald-100", accent: darkMode ? "bg-emerald-300" : "bg-emerald-700", }, }, rose: { name: "Rose Theme", theme: { colors: { primary: "flow-bg-rose-600 dark:flow-bg-rose-400", primaryForeground: "flow-text-white", secondary: "flow-bg-rose-100 dark:flow-bg-rose-900", secondaryForeground: "flow-text-rose-900 dark:flow-text-rose-100", accent: "flow-bg-rose-700 dark:flow-bg-rose-300", background: "flow-bg-white dark:flow-bg-rose-950", foreground: "flow-text-rose-900 dark:flow-text-rose-100", muted: "flow-bg-rose-50 dark:flow-bg-rose-800", mutedForeground: "flow-text-rose-500 dark:flow-text-rose-400", border: "flow-border-rose-200 dark:flow-border-rose-700", link: "flow-text-rose-600 dark:flow-text-rose-300", }, }, colors: { primary: darkMode ? "bg-rose-400" : "bg-rose-600", secondary: darkMode ? "bg-rose-800" : "bg-rose-100", accent: darkMode ? "bg-rose-300" : "bg-rose-700", }, }, } const currentTheme = themes[selectedTheme as keyof typeof themes] return (

Theme Customization Demo

Primary
Secondary
Accent

Live Flow Components Preview:

[ arg("Hello from themed component!", t.String), ], limit: 999, }} mutation={{ onSuccess: data => { console.log("Themed transaction succeeded:", data) }, onError: error => { console.error("Themed transaction failed:", error) }, }} />

Theming Features

  • • Custom color palettes and branding
  • • Tailwind CSS utility class overrides
  • • Light and dark theme variants
  • • Consistent styling across components
) } ================================================ FILE: packages/demo/src/components/component-cards/connect-card.tsx ================================================ import {useState} from "react" import {Connect, useFlowChainId} from "@onflow/react-sdk" import {useDarkMode} from "../flow-provider-wrapper" import {DemoCard, type PropDefinition} from "../ui/demo-card" import {PlusGridIcon} from "../ui/plus-grid" import {CONTRACT_ADDRESSES} from "../../constants" const IMPLEMENTATION_CODE = `import { Connect } from "@onflow/react-sdk" // Basic usage - shows FLOW by default // With multiple tokens - dropdown selector, first token is default // Note: Provide only ONE identifier per token (the bridge derives the other) ` const PROPS: PropDefinition[] = [ { name: "variant", type: '"primary" | "secondary" | "outline" | "link"', required: false, description: "The visual style variant of the connect button", defaultValue: '"primary"', }, { name: "onConnect", type: "() => void", required: false, description: "Callback function called when user connects their wallet", }, { name: "onDisconnect", type: "() => void", required: false, description: "Callback function called when user disconnects their wallet", }, { name: "balanceType", type: '"cadence" | "evm" | "combined"', required: false, description: "Type of balance to display: cadence (Cadence VM only), evm (EVM only), or combined (sum of both)", defaultValue: '"cadence"', }, { name: "balanceTokens", type: "TokenConfig[]", required: false, description: "Array of tokens with dropdown selector (first token is default). Each token needs symbol, name, and EXACTLY ONE of: vaultIdentifier OR erc20Address (the bridge derives the other automatically)", }, { name: "modalConfig", type: "ConnectModalConfig", required: false, description: "Configuration for the profile modal (like show scheduled transactions, filter handler types)", }, { name: "modalEnabled", type: "boolean", required: false, description: "Controls whether the profile modal opens on click when connected. Set to false to manage the profile display externally.", defaultValue: "true", }, ] export function ConnectCard() { const {darkMode} = useDarkMode() const {data: chainId, isLoading} = useFlowChainId() const isEmulator = chainId === "emulator" || chainId === "local" const [showMultiToken, setShowMultiToken] = useState(false) const [modalEnabled, setModalEnabled] = useState(true) const [balanceType, setBalanceType] = useState< "cadence" | "evm" | "combined" >("cadence") const getFlowTokenAddress = () => { if (chainId === "emulator" || chainId === "local") { return CONTRACT_ADDRESSES.FlowToken.emulator } return chainId === "testnet" ? CONTRACT_ADDRESSES.FlowToken.testnet : CONTRACT_ADDRESSES.FlowToken.mainnet } const multiTokens = [ { symbol: "FLOW", name: "Flow Token", vaultIdentifier: `A.${getFlowTokenAddress().replace("0x", "")}.FlowToken.Vault`, }, // Only show USDF (PYUSD) on testnet and mainnet // Note: Only vaultIdentifier provided - EVM address is derived by the bridge ...(!isEmulator ? [ { symbol: "USDF", name: "USDF (PYUSD)", vaultIdentifier: chainId === "testnet" ? "A.dfc20aee650fcbdf.EVMVMBridgedToken_f2e5a325f7d678da511e66b1c0ad7d5ba4df93d3.Vault" : "A.1e4aa0b87d10b141.EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabed.Vault", }, ] : []), ] return (

Authentication

Built-in wallet flow

Multi-Token Cross-VM

Multi-token support with cross-VM bridge integration. Provide only a vaultIdentifier or an erc20Address and the bridge derives the other automatically.

{isLoading ? (
) : isEmulator ? (

Emulator Network Detected

Connect component requires testnet or mainnet

) : (
{showMultiToken ? ( ) : ( )}
)}
{!isEmulator && (
{(["cadence", "evm", "combined"] as const).map(type => ( ))}
)}
) } ================================================ FILE: packages/demo/src/components/component-cards/demo-nft-card.tsx ================================================ import {NftCard, type NftCardAction, useFlowChainId} from "@onflow/react-sdk" import {useState} from "react" import {useDarkMode} from "../flow-provider-wrapper" import {DemoCard, type PropDefinition} from "../ui/demo-card" import {PlusGridIcon} from "../ui/plus-grid" import {DEMO_ADDRESS_TESTNET} from "../../constants" const IMPLEMENTATION_CODE = `import { NftCard } from "@onflow/react-sdk" { // Handle transfer logic console.log("Transfer NFT") } }, { title: "List for Sale", onClick: () => { // Handle listing logic console.log("List NFT for sale") } }, { title: "View on Explorer", onClick: () => { window.open("https://flowscan.io", "_blank") } } ]} />` const PROPS: PropDefinition[] = [ { name: "accountAddress", type: "string", required: true, description: "The Flow account address that owns the NFT", }, { name: "tokenId", type: "string | number", required: true, description: "The unique identifier of the NFT within the collection", }, { name: "publicPathIdentifier", type: "string", required: true, description: "The public path identifier for the NFT collection", }, { name: "showTraits", type: "boolean", required: false, description: "Display NFT traits and attributes when available (default: false)", }, { name: "showExtra", type: "boolean", required: false, description: "Show additional metadata like serial number, rarity, and external links (default: false)", }, { name: "actions", type: "NftCardAction[]", required: false, description: "Array of custom action buttons to display in a 3-dot menu. Each action has a title and onClick handler.", }, { name: "className", type: "string", required: false, description: "Additional CSS classes to apply to the card container for custom styling", }, { name: "style", type: "React.CSSProperties", required: false, description: "Inline styles to apply to the card container for custom styling", }, ] export function DemoNftCard() { const {darkMode} = useDarkMode() const {data: chainId, isLoading} = useFlowChainId() const [accountAddress, setAccountAddress] = useState("") const [tokenId, setTokenId] = useState("") const [publicPath, setPublicPath] = useState("") const [showTraits, setShowTraits] = useState(true) const [showExtra, setShowExtra] = useState(true) const [showActions, setShowActions] = useState(true) const isEmulator = chainId === "emulator" || chainId === "local" const isTestnet = chainId === "testnet" const isMainnet = chainId === "mainnet" const canShowDemo = isTestnet || isMainnet // Use user input if provided, otherwise use demo values const displayAddress = accountAddress || DEMO_ADDRESS_TESTNET const displayTokenId = tokenId || "8" const displayPublicPath = publicPath || "exampleNFTCollection" return (

NFT Metadata

Fetches NFT data automatically

Rich Display

Shows traits and attributes

Custom Actions

Extensible 3-dot menu

{isLoading ? (
) : isEmulator ? (

Emulator Network Detected

NFT Card component requires testnet or mainnet

) : !canShowDemo ? (

Demo Available on Testnet and Mainnet

Switch to testnet or mainnet to see the NFT card demo

) : (
setAccountAddress(e.target.value)} className={`w-full px-3 py-2 rounded-lg border ${ darkMode ? "bg-gray-900/50 border-white/10 text-white" : "bg-white border-black/10 text-black" } focus:outline-none focus:ring-2 focus:ring-flow-primary/50`} placeholder={DEMO_ADDRESS_TESTNET} />
setTokenId(e.target.value)} className={`w-full px-3 py-2 rounded-lg border ${ darkMode ? "bg-gray-900/50 border-white/10 text-white" : "bg-white border-black/10 text-black" } focus:outline-none focus:ring-2 focus:ring-flow-primary/50`} placeholder="8" />
setPublicPath(e.target.value)} className={`w-full px-3 py-2 rounded-lg border ${ darkMode ? "bg-gray-900/50 border-white/10 text-white" : "bg-white border-black/10 text-black" } focus:outline-none focus:ring-2 focus:ring-flow-primary/50`} placeholder="exampleNFTCollection" />
{ alert( "Transfer action clicked! In a real app, this would open a transfer dialog." ) }, }, { title: "List for Sale", onClick: () => { alert( "List for Sale action clicked! In a real app, this would open a listing dialog." ) }, }, { title: "View on FlowDiver", onClick: () => { const network = chainId === "mainnet" ? "mainnet" : "testnet" window.open( `https://www.flowdiver.io/account/${displayAddress}`, "_blank" ) }, }, { title: "Copy Token ID", onClick: () => { navigator.clipboard.writeText(displayTokenId) alert( `Token ID ${displayTokenId} copied to clipboard!` ) }, }, ] : undefined } />

Display Options

)}
) } ================================================ FILE: packages/demo/src/components/component-cards/profile-card.tsx ================================================ import {useState} from "react" import {Profile, useFlowChainId} from "@onflow/react-sdk" import {useDarkMode} from "../flow-provider-wrapper" import {DemoCard, type PropDefinition} from "../ui/demo-card" import {PlusGridIcon} from "../ui/plus-grid" import {CONTRACT_ADDRESSES} from "../../constants" const IMPLEMENTATION_CODE = `import { Profile } from "@onflow/react-sdk" // Basic usage - standalone profile display // With custom configuration console.log("User disconnected")} balanceTokens={[ { symbol: "FLOW", name: "Flow Token", vaultIdentifier: "A.1654653399040a61.FlowToken.Vault", }, { symbol: "USDF", name: "USDF (PYUSD)", vaultIdentifier: "A.1e4aa0b87d10b141.EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabed.Vault", }, ]} profileConfig={{ scheduledTransactions: { show: true, filterHandlerTypes: ["A.123.Contract.Handler"], } }} />` const PROPS: PropDefinition[] = [ { name: "onDisconnect", type: "() => void", required: false, description: "Callback function called when user disconnects their wallet", }, { name: "balanceType", type: '"cadence" | "evm" | "combined"', required: false, description: "Type of balance to display: cadence (Cadence VM only), evm (EVM only), or combined (sum of both)", defaultValue: '"cadence"', }, { name: "balanceTokens", type: "TokenConfig[]", required: false, description: "Array of tokens with dropdown selector (first token is default). Each token needs symbol, name, and EXACTLY ONE of: vaultIdentifier OR erc20Address (the bridge derives the other automatically)", }, { name: "profileConfig", type: "ProfileConfig", required: false, description: "Configuration for the profile (like show scheduled transactions, filter handler types)", }, { name: "className", type: "string", required: false, description: "Additional CSS classes to apply to the profile container", }, { name: "style", type: "React.CSSProperties", required: false, description: "Inline styles to apply to the profile container", }, ] export function ProfileCard() { const {darkMode} = useDarkMode() const {data: chainId, isLoading} = useFlowChainId() const isEmulator = chainId === "emulator" || chainId === "local" const [showMultiToken, setShowMultiToken] = useState(false) const [showScheduledTxs, setShowScheduledTxs] = useState(false) const [balanceType, setBalanceType] = useState< "cadence" | "evm" | "combined" >("cadence") const getFlowTokenAddress = () => { if (chainId === "emulator" || chainId === "local") { return CONTRACT_ADDRESSES.FlowToken.emulator } return chainId === "testnet" ? CONTRACT_ADDRESSES.FlowToken.testnet : CONTRACT_ADDRESSES.FlowToken.mainnet } const multiTokens = [ { symbol: "FLOW", name: "Flow Token", vaultIdentifier: `A.${getFlowTokenAddress().replace("0x", "")}.FlowToken.Vault`, }, // Only show USDF (PYUSD) on testnet and mainnet ...(!isEmulator ? [ { symbol: "USDF", name: "USDF (PYUSD)", vaultIdentifier: chainId === "testnet" ? "A.dfc20aee650fcbdf.EVMVMBridgedToken_f2e5a325f7d678da511e66b1c0ad7d5ba4df93d3.Vault" : "A.1e4aa0b87d10b141.EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabed.Vault", }, ] : []), ] return (

Standalone

Reusable anywhere

Auto-detects State

Shows connected/not connected

Multi-Token Cross-VM

Cross-VM balance with token selector

{isLoading ? (
) : isEmulator ? (

Emulator Network Detected

Profile component requires testnet or mainnet

) : (
console.log("User disconnected")} />
)}
{!isEmulator && (
{(["cadence", "evm", "combined"] as const).map(type => ( ))}
)}
) } ================================================ FILE: packages/demo/src/components/component-cards/scheduled-transaction-list-demo.tsx ================================================ import { ScheduledTransactionList, useFlowCurrentUser, useFlowChainId, } from "@onflow/react-sdk" import {useState} from "react" import {useDarkMode} from "../flow-provider-wrapper" import {DemoCard, type PropDefinition} from "../ui/demo-card" import {PlusGridIcon} from "../ui/plus-grid" import {DEMO_ADDRESS_TESTNET} from "../../constants" const IMPLEMENTATION_CODE = `import { ScheduledTransactionList, useFlowCurrentUser } from "@onflow/react-sdk" function MyComponent() { const { user } = useFlowCurrentUser() return (
) }` const PROPS: PropDefinition[] = [ { name: "address", type: "string", required: true, description: "The Flow account address to fetch scheduled transactions for", }, { name: "filterHandlerTypes", type: "string[]", required: false, description: "Array of handler type identifiers to filter. Only transactions matching these types will be displayed", }, { name: "cancelEnabled", type: "boolean", required: false, description: "Whether to show cancel buttons on transaction cards. Defaults to true", }, { name: "className", type: "string", required: false, description: "Additional CSS classes to apply to the list container", }, { name: "style", type: "React.CSSProperties", required: false, description: "Inline styles to apply to the list container", }, ] export function ScheduledTransactionListDemo() { const {darkMode} = useDarkMode() const {user} = useFlowCurrentUser() const {data: chainId, isLoading} = useFlowChainId() const isEmulator = chainId === "emulator" || chainId === "local" const [customAddress, setCustomAddress] = useState("") const [filterInput, setFilterInput] = useState("") const normalizeAddress = (address: string): string => { if (!address) return "" const trimmed = address.trim() return trimmed.startsWith("0x") ? trimmed : `0x${trimmed}` } const displayAddress = customAddress ? normalizeAddress(customAddress) : chainId === "testnet" ? DEMO_ADDRESS_TESTNET : user?.addr || "" const filterHandlerTypes = filterInput ? filterInput .split(",") .map(s => s.trim()) .filter(s => s.length > 0) : undefined return (

Auto-fetch

Automatic data fetching

Scrollable

Configurable max height

Auto-refresh

Refreshes after cancellation

setCustomAddress(e.target.value)} placeholder={user?.addr || "Enter Flow address (e.g., 0x...)"} className={`flex-1 px-3 py-2 rounded-md border text-sm font-mono ${ darkMode ? `bg-gray-800 border-gray-700 text-white placeholder-gray-500 focus:border-blue-500` : `bg-white border-gray-300 text-gray-900 placeholder-gray-400 focus:border-blue-500` } focus:outline-none focus:ring-1 focus:ring-blue-500`} /> {customAddress && ( )}
{!customAddress && (

{chainId === "testnet" ? `Using demo account: ${DEMO_ADDRESS_TESTNET}` : user?.addr ? `Using connected wallet address: ${user.addr}` : "Connect wallet or enter address to view scheduled transactions"}

)}
setFilterInput(e.target.value)} placeholder="e.g., A.123.Contract.Handler1, A.456.Contract.Handler2" className={`flex-1 px-3 py-2 rounded-md border text-sm font-mono ${ darkMode ? `bg-gray-800 border-gray-700 text-white placeholder-gray-500 focus:border-blue-500` : `bg-white border-gray-300 text-gray-900 placeholder-gray-400 focus:border-blue-500` } focus:outline-none focus:ring-1 focus:ring-blue-500`} /> {filterInput && ( )}

{filterInput ? `Filtering by ${filterHandlerTypes?.length || 0} handler type(s)` : "Enter comma-separated handler type identifiers to filter transactions"}

{isLoading ? (

Loading chain information...

) : isEmulator ? (

Emulator Network Detected

Scheduled transactions require testnet or mainnet

) : !displayAddress ? (

Connect Wallet or Enter Address

Connect your wallet or enter a Flow address above to view scheduled transactions

) : (
)}
) } ================================================ FILE: packages/demo/src/components/component-cards/transaction-button-card.tsx ================================================ import {TransactionButton} from "@onflow/react-sdk" import {useState} from "react" import {useDarkMode} from "../flow-provider-wrapper" import {DemoCard, type PropDefinition} from "../ui/demo-card" import {PlusGridIcon} from "../ui/plus-grid" const IMPLEMENTATION_CODE = `import { TransactionButton } from "@onflow/react-sdk" [arg("Hello, World!", t.String)], limit: 999, }} mutation={{ onSuccess: (data) => { console.log("Transaction ID:", data) }, onError: (error) => { console.error("Transaction failed:", error) } }} />` const PROPS: PropDefinition[] = [ { name: "transaction", type: "Parameters[0]", required: true, description: "Transaction configuration object that can be passed to fcl.mutate", }, { name: "label", type: "string", required: false, description: "Optional label for the button (defaults to 'Execute Transaction')", }, { name: "mutation", type: "UseMutationOptions[0]>", required: false, description: "TanStack React Query mutation options for customizing behavior", }, { name: "variant", type: '"primary" | "secondary" | "outline" | "link"', required: false, description: "The visual style variant of the button", defaultValue: '"primary"', }, { name: "disabled", type: "boolean", required: false, description: "Whether the button is disabled", }, ] export function TransactionButtonCard() { const {darkMode} = useDarkMode() const [transactionResult, setTransactionResult] = useState(null) const GREETING_TRANSACTION = ` transaction(greeting: String) { prepare(signer: &Account) { log(greeting) } } ` return (

Transaction Signing

Built-in wallet integration

Status Management

Loading and error states

[arg("Hello, World!", t.String)], limit: 999, }} mutation={{ onSuccess: data => { setTransactionResult({ txId: data, timestamp: new Date().toISOString(), }) }, onError: error => { setTransactionResult({ error: error.message, timestamp: new Date().toISOString(), }) }, }} />
{transactionResult && ( <>

Transaction Result

{transactionResult.txId ? (

Transaction Submitted Successfully

Transaction ID:

{transactionResult.txId}

{new Date(transactionResult.timestamp).toLocaleString()}

) : (

Transaction Failed

{transactionResult.error}

{new Date(transactionResult.timestamp).toLocaleString()}

)}
)}
) } ================================================ FILE: packages/demo/src/components/component-cards/transaction-dialog-card.tsx ================================================ import {TransactionButton, TransactionDialog} from "@onflow/react-sdk" import {useState} from "react" import {useDarkMode} from "../flow-provider-wrapper" import {DemoCard, type PropDefinition} from "../ui/demo-card" import {PlusGridIcon} from "../ui/plus-grid" const IMPLEMENTATION_CODE = `import { TransactionDialog } from "@onflow/react-sdk" ` const PROPS: PropDefinition[] = [ { name: "open", type: "boolean", required: true, description: "Controls whether the dialog is open or closed", }, { name: "onOpenChange", type: "(open: boolean) => void", required: true, description: "Callback function called when the dialog open state changes", }, { name: "txId", type: "string", required: false, description: "Transaction ID to display status for", }, { name: "onSuccess", type: "() => void", required: false, description: "Callback function called when transaction succeeds", }, { name: "pendingTitle", type: "string", required: false, description: "Custom title for pending state (defaults to 'Transaction Pending')", }, { name: "pendingDescription", type: "string", required: false, description: "Custom description for pending state", }, { name: "successTitle", type: "string", required: false, description: "Custom title for success state (defaults to 'Transaction Successful')", }, { name: "successDescription", type: "string", required: false, description: "Custom description for success state", }, { name: "closeOnSuccess", type: "boolean", required: false, description: "Whether to automatically close dialog when transaction succeeds", }, ] export function TransactionDialogCard() { const {darkMode} = useDarkMode() const [open, setOpen] = useState(false) const [txId, setTxId] = useState(undefined) const [transactionResult, setTransactionResult] = useState(null) const GREETING_TRANSACTION = ` transaction(greeting: String) { prepare(signer: &Account) { log(greeting) } } ` return (

Real-time Updates

Live transaction status

Modal Interface

Overlay with progress

Dialog Flow

1
Sign Transaction
2
Dialog Opens
3
Status Updates
[arg("Hello, World!", t.String)], limit: 999, }} mutation={{ onSuccess: data => { setTxId(data) setOpen(true) setTransactionResult({ txId: data, timestamp: new Date().toISOString(), dialogShown: true, }) }, onError: error => { setTransactionResult({ error: error.message, timestamp: new Date().toISOString(), dialogShown: false, }) }, }} />
{txId && !open && (

Transaction submitted! Dialog was displayed.

)}
) } ================================================ FILE: packages/demo/src/components/component-cards/transaction-link-card.tsx ================================================ import {TransactionButton, TransactionLink} from "@onflow/react-sdk" import {useState} from "react" import {useDarkMode} from "../flow-provider-wrapper" import {DemoCard, type PropDefinition} from "../ui/demo-card" import {PlusGridIcon} from "../ui/plus-grid" const IMPLEMENTATION_CODE = `import { TransactionLink } from "@onflow/react-sdk" ` const PROPS: PropDefinition[] = [ { name: "txId", type: "string", required: true, description: "Transaction ID to create block explorer link for", }, { name: "variant", type: '"primary" | "secondary" | "outline" | "link"', required: false, description: "The visual style variant of the link button", defaultValue: '"link"', }, ] export function TransactionLinkCard() { const {darkMode} = useDarkMode() const [txId, setTxId] = useState(undefined) const [transactionResult, setTransactionResult] = useState(null) const GREETING_TRANSACTION = ` transaction(greeting: String) { prepare(signer: &Account) { log(greeting) } } ` return (

Block Explorer

Direct links to flowscan

Network Aware

Auto network detection

[arg("Hello, World!", t.String)], limit: 999, }} mutation={{ onSuccess: data => { setTxId(data) setTransactionResult({ txId: data, timestamp: new Date().toISOString(), linkGenerated: true, }) }, onError: error => { setTransactionResult({ error: error.message, timestamp: new Date().toISOString(), linkGenerated: false, }) }, }} />
{txId && (

Transaction Submitted!

Transaction ID: {txId}
)} {!txId && (

Submit a transaction to see the block explorer link

)}
{transactionResult && ( <>

Transaction Result

{transactionResult.txId ? (

Transaction Submitted & Link Generated

Transaction ID:

{transactionResult.txId}

{new Date(transactionResult.timestamp).toLocaleString()}

) : (

Transaction Failed

{transactionResult.error}

{new Date(transactionResult.timestamp).toLocaleString()}

)}
)}
) } ================================================ FILE: packages/demo/src/components/content-section.tsx ================================================ import {PlusGrid, PlusGridRow} from "./ui/plus-grid" import {StarterBanner} from "./starter-banner" // Import hook cards import {UseFlowCurrentUserCard} from "./hook-cards/use-flow-current-user-card" import {UseFlowAccountCard} from "./hook-cards/use-flow-account-card" import {UseFlowBlockCard} from "./hook-cards/use-flow-block-card" import {UseFlowChainIdCard} from "./hook-cards/use-flow-chain-id-card" import {UseFlowConfigCard} from "./hook-cards/use-flow-config-card" import {UseFlowQueryCard} from "./hook-cards/use-flow-query-card" import {UseFlowQueryRawCard} from "./hook-cards/use-flow-query-raw-card" import {UseFlowMutateCard} from "./hook-cards/use-flow-mutate-card" import {UseFlowEventsCard} from "./hook-cards/use-flow-events-card" import {UseFlowTransactionStatusCard} from "./hook-cards/use-flow-transaction-status-card" import {UseFlowRevertibleRandomCard} from "./hook-cards/use-flow-revertible-random-card" import {UseFlowScheduledTransactionCard} from "./hook-cards/use-flow-scheduled-transaction-card" import {UseCrossVmBridgeNftFromEvmCard} from "./hook-cards/use-cross-vm-bridge-nft-from-evm-card" import {UseCrossVmBridgeNftToEvmCard} from "./hook-cards/use-cross-vm-bridge-nft-to-evm-card" import {UseCrossVmBridgeTokenFromEvmCard} from "./hook-cards/use-cross-vm-bridge-token-from-evm-card" import {UseCrossVmBridgeTokenToEvmCard} from "./hook-cards/use-cross-vm-bridge-token-to-evm-card" import {UseFlowNftMetadataCard} from "./hook-cards/use-flow-nft-metadata-card" // Import setup cards import {InstallationCard} from "./setup-cards/installation-card" // Import advanced cards import {DarkModeCard} from "./advanced-cards/dark-mode-card" import {ThemingCard} from "./advanced-cards/theming-card" // Import component cards import {ConnectCard} from "./component-cards/connect-card" import {ProfileCard} from "./component-cards/profile-card" import {TransactionButtonCard} from "./component-cards/transaction-button-card" import {TransactionDialogCard} from "./component-cards/transaction-dialog-card" import {TransactionLinkCard} from "./component-cards/transaction-link-card" import {DemoNftCard} from "./component-cards/demo-nft-card" import {ScheduledTransactionListDemo} from "./component-cards/scheduled-transaction-list-demo" export function ContentSection() { return (

Getting Started

Install and configure the Flow React SDK to start building apps on Flow.

React Components

Pre-built UI components for common Flow blockchain interactions. Drop them into your app to ship faster.

React Hooks

Powerful React hooks for interacting with the Flow blockchain. Each hook provides a simple interface for complex blockchain operations.

Advanced Features

Customize and extend the Flow React SDK with advanced theming, dark mode controls, and configuration options.

) } ================================================ FILE: packages/demo/src/components/content-sidebar.tsx ================================================ import {useState, useEffect, useRef} from "react" import {PlusGridIcon} from "./ui/plus-grid" interface SidebarItem { id: string label: string category: "setup" | "components" | "hooks" | "advanced" description: string } const sidebarItems: SidebarItem[] = [ // Setup section { id: "installation", label: "Installation & Setup", category: "setup", description: "Install and configure the React SDK", }, // Components section { id: "connect", label: "Connect", category: "components", description: "Wallet connection component", }, { id: "profile", label: "Profile", category: "components", description: "Standalone profile display", }, { id: "transactionbutton", label: "Transaction Button", category: "components", description: "Transaction execution button", }, { id: "transactiondialog", label: "Transaction Dialog", category: "components", description: "Transaction confirmation dialog", }, { id: "transactionlink", label: "Transaction Link", category: "components", description: "Transaction link component", }, { id: "nftcard", label: "NFT Card", category: "components", description: "NFT card component", }, { id: "scheduledtransactionlist", label: "Scheduled Transaction List", category: "components", description: "List of scheduled transactions", }, // Hooks section { id: "useflowcurrentuser", label: "Current User", category: "hooks", description: "Manage user authentication", }, { id: "useflowaccount", label: "Account", category: "hooks", description: "Fetch account information", }, { id: "useflowblock", label: "Block", category: "hooks", description: "Get blockchain block data", }, { id: "useflowchainid", label: "Chain ID", category: "hooks", description: "Get current chain ID", }, { id: "useflowconfig", label: "Config", category: "hooks", description: "Access Flow configuration", }, { id: "useflowquery", label: "Query", category: "hooks", description: "Execute Flow scripts", }, { id: "useflowqueryraw", label: "Query Raw", category: "hooks", description: "Execute raw Flow scripts", }, { id: "useflowmutate", label: "Mutate", category: "hooks", description: "Send Flow transactions", }, { id: "useflowevents", label: "Events", category: "hooks", description: "Listen to blockchain events", }, { id: "useflowrevertiblerandom", label: "Revertible Random", category: "hooks", description: "Generate random numbers", }, { id: "useflowtransactionstatus", label: "Transaction Status", category: "hooks", description: "Track transaction status", }, { id: "usecrossvmbridgenftfromevm", label: "Bridge NFT from EVM", category: "hooks", description: "Bridge NFTs from EVM to Cadence", }, { id: "usecrossvmbridgenfttoevm", label: "Bridge NFT to EVM", category: "hooks", description: "Bridge NFTs from Cadence to EVM", }, { id: "usecrossvmbridgetokenfromevm", label: "Bridge Token from EVM", category: "hooks", description: "Bridge tokens from EVM to Cadence", }, { id: "usecrossvmbridgetokentoevm", label: "Bridge Token to EVM", category: "hooks", description: "Bridge tokens from Cadence to EVM", }, { id: "useflownftmetadata", label: "NFT Metadata", category: "hooks", description: "Fetch NFT metadata and traits", }, { id: "useflowscheduledtransaction", label: "Scheduled Transactions", category: "hooks", description: "Manage Scheduled Transactions", }, // Advanced section { id: "darkmode", label: "Dark Mode Control", category: "advanced", description: "Dynamic theme switching", }, { id: "theming", label: "Custom Theming", category: "advanced", description: "Customize component appearance", }, ] interface ContentSidebarProps { darkMode: boolean onItemClick?: () => void } export function ContentSidebar({darkMode, onItemClick}: ContentSidebarProps) { const [activeSection, setActiveSection] = useState("") const navRef = useRef(null) const scrollLockRef = useRef(null) const setupItems = sidebarItems.filter(item => item.category === "setup") const componentsItems = sidebarItems.filter( item => item.category === "components" ) const hooksItems = sidebarItems.filter(item => item.category === "hooks") const advancedItems = sidebarItems.filter( item => item.category === "advanced" ) const scrollToElement = (id: string, isInitialLoad = false) => { const element = document.getElementById(id) if (element) { // Save current sidebar scroll position if nav ref is available (but not on initial load) if (navRef.current && !isInitialLoad) { scrollLockRef.current = navRef.current.scrollTop } // Update the URL hash without reloading the page window.history.replaceState(null, "", `#${id}`) // Calculate position manually to ensure consistent 60px from top const absoluteElementTop = element.offsetTop const scrollMarginTop = 60 const targetPosition = absoluteElementTop - scrollMarginTop const maxScroll = document.documentElement.scrollHeight - window.innerHeight // Use the minimum of target position and max scroll to handle elements at the bottom const scrollPosition = Math.min(targetPosition, maxScroll) window.scrollTo({ top: scrollPosition, behavior: "smooth", }) // Call onItemClick callback (for closing mobile sidebar) if (!isInitialLoad && onItemClick) { onItemClick() } // Unlock after scroll animation completes (only if we locked it) if (!isInitialLoad) { setTimeout(() => { scrollLockRef.current = null }, 1000) } } } // Prevent sidebar from scrolling when clicking buttons useEffect(() => { const nav = navRef.current if (!nav) return const handleScroll = () => { if (scrollLockRef.current !== null) { nav.scrollTop = scrollLockRef.current } } nav.addEventListener("scroll", handleScroll) return () => nav.removeEventListener("scroll", handleScroll) }, []) // Track which section is currently visible useEffect(() => { // Check if there's a hash in the URL on initial load const hash = window.location.hash.slice(1) // Remove the # character if (hash) { setActiveSection(hash) // Manually trigger scroll since browser isn't doing it automatically const scrollToHash = () => { const element = document.getElementById(hash) if (element) { // Use scrollIntoView with auto behavior (instant, not smooth) that respects scroll-margin-top element.scrollIntoView({behavior: "auto", block: "start"}) } } // Try multiple times to ensure content is loaded requestAnimationFrame(() => { scrollToHash() setTimeout(scrollToHash, 100) setTimeout(scrollToHash, 300) setTimeout(scrollToHash, 500) }) } const observer = new IntersectionObserver( entries => { // Find the entry with the highest intersection ratio that's actually visible const visibleEntries = entries.filter(entry => entry.isIntersecting) if (visibleEntries.length > 0) { // Sort by intersection ratio and pick the most visible one const mostVisible = visibleEntries.reduce((prev, current) => current.intersectionRatio > prev.intersectionRatio ? current : prev ) setActiveSection(mostVisible.target.id) } }, { threshold: [0.1, 0.3, 0.5, 0.7, 0.9], rootMargin: "-80px 0px -40% 0px", // Account for header height and give more weight to top sections } ) sidebarItems.forEach(item => { const element = document.getElementById(item.id) if (element) { observer.observe(element) } }) return () => observer.disconnect() }, []) const SidebarSection = ({ title, items, icon, }: { title: string items: SidebarItem[] icon: React.ReactNode }) => (
{icon}

{title}

    {items.map(item => { const isActive = activeSection === item.id return (
  • ) })}
) return (
} /> } /> } /> } />

Quick Links

Documentation GitHub
) } ================================================ FILE: packages/demo/src/components/flow-provider-wrapper.tsx ================================================ import * as fcl from "@onflow/fcl" import {FlowProvider} from "@onflow/react-sdk" import React, {createContext, useCallback, useContext, useState} from "react" // Dark mode context interface DarkModeContextType { darkMode: boolean toggleDarkMode: () => void } // Network switching context interface NetworkSwitchContextType { currentNetwork: DemoFlowNetwork switchNetwork: (network: DemoFlowNetwork) => Promise isNetworkSwitching: boolean } // Flow configuration type interface FlowConfig { accessNodeUrl: string discoveryWallet: string discoveryAuthnEndpoint: string flowNetwork: string } const DarkModeContext = createContext( undefined ) const NetworkSwitchContext = createContext< NetworkSwitchContextType | undefined >(undefined) export const useDarkMode = () => { const context = useContext(DarkModeContext) if (!context) { throw new Error("useDarkMode must be used within a DarkModeProvider") } return context } export const useNetworkSwitch = () => { const context = useContext(NetworkSwitchContext) if (!context) { throw new Error( "useNetworkSwitch must be used within a NetworkSwitchProvider" ) } return context } // Standard networks supported by the demo type DemoFlowNetwork = "emulator" | "testnet" | "mainnet" const flowNetwork = (import.meta.env.VITE_FLOW_NETWORK as DemoFlowNetwork) || "emulator" const flowConfig: Record = { emulator: { accessNodeUrl: "http://localhost:8888", discoveryWallet: "http://localhost:8701/fcl/authn", discoveryAuthnEndpoint: "http://localhost:8701/fcl/authn", flowNetwork: "local" as const, }, testnet: { accessNodeUrl: "https://rest-testnet.onflow.org", discoveryWallet: "https://fcl-discovery.onflow.org/testnet/authn", discoveryAuthnEndpoint: "https://fcl-discovery.onflow.org/api/testnet/authn", flowNetwork: "testnet" as const, }, mainnet: { accessNodeUrl: "https://rest-mainnet.onflow.org", discoveryWallet: "https://fcl-discovery.onflow.org/mainnet/authn", discoveryAuthnEndpoint: "https://fcl-discovery.onflow.org/api/mainnet/authn", flowNetwork: "mainnet" as const, }, } // Network switching provider component function NetworkSwitchProvider({children}: {children: React.ReactNode}) { const [currentNetwork, setCurrentNetwork] = useState(flowNetwork) const [isNetworkSwitching, setIsNetworkSwitching] = useState(false) const switchNetwork = useCallback( async (network: DemoFlowNetwork) => { setIsNetworkSwitching(true) try { // Check if user is currently authenticated const currentUser = await fcl.currentUser.snapshot() const wasAuthenticated = currentUser.loggedIn // If user is authenticated, unauthenticate them first if (wasAuthenticated) { fcl.unauthenticate() } // Update FCL configuration for new network const config = flowConfig[network] fcl.config({ "accessNode.api": config.accessNodeUrl, "discovery.wallet": config.discoveryWallet, "flow.network": config.flowNetwork, "app.detail.title": "Demo App", "app.detail.url": window.location.origin, "app.detail.icon": "https://avatars.githubusercontent.com/u/62387156?v=4", "fcl.limit": 1000, "fcl.walletconnect.projectId": "9b70cfa398b2355a5eb9b1cf99f4a981", }) // Update local state setCurrentNetwork(network) } catch (error) { console.error("Failed to switch network:", error) } finally { setIsNetworkSwitching(false) } }, [currentNetwork] ) return ( {children} ) } // Inner component that can access the network switch context function FlowProviderInner({ children, darkMode, }: { children: React.ReactNode darkMode: boolean }) { const {currentNetwork} = useNetworkSwitch() return ( {children} ) } export default function FlowProviderWrapper({ children, darkMode, toggleDarkMode, }: { children: React.ReactNode darkMode: boolean toggleDarkMode: () => void }) { return ( {children} ) } ================================================ FILE: packages/demo/src/components/footer.tsx ================================================ import {PlusGridRow, PlusGridItem} from "./ui/plus-grid" import {useDarkMode} from "./flow-provider-wrapper" export function Footer() { const {darkMode} = useDarkMode() const footerLinks = { "Developer Resources": [ { name: "FCL Documentation", href: "https://developers.flow.com/tools/fcl-js", }, { name: "React SDK Docs", href: "https://developers.flow.com/tools/fcl-js/react-sdk", }, {name: "Flow CLI", href: "https://developers.flow.com/tools/flow-cli"}, {name: "Cadence Language", href: "https://developers.flow.com/cadence"}, ], "Flow Network": [ {name: "Flow Blockchain", href: "https://flow.com"}, {name: "Flow Explorer", href: "https://flowscan.org"}, {name: "Flow Port", href: "https://port.onflow.org"}, {name: "Flow Faucet", href: "https://testnet-faucet.onflow.org"}, ], Community: [ { name: "GitHub", href: "https://github.com/onflow/fcl-js/tree/master/packages/react-sdk", }, {name: "Discord", href: "https://discord.gg/flow"}, {name: "Forum", href: "https://forum.onflow.org"}, {name: "Twitter", href: "https://twitter.com/flow_blockchain"}, ], "Tools & APIs": [ { name: "Flow Client Library", href: "https://developers.flow.com/tools/fcl-js", }, {name: "Access API", href: "https://developers.flow.com/http-api"}, {name: "Emulator", href: "https://developers.flow.com/tools/emulator"}, { name: "NFT Standards", href: "https://developers.flow.com/cadence/tutorial/non-fungible-tokens", }, ], } return (
Flow
Flow

React SDK

The Flow React SDK provides everything you need to build amazing Web3 applications. Start building with ready to use components and hooks and ship faster.

{Object.entries(footerLinks).map(([category, links]) => (

{category}

    {links.map(link => (
  • {link.name}
  • ))}
))}

Stay Updated

Get the latest updates on Flow development tools and ecosystem news.

Subscribe
Built with ♥ for developers.
) } ================================================ FILE: packages/demo/src/components/header.tsx ================================================ import {Connect, useFlowChainId} from "@onflow/react-sdk" import {useDarkMode, useNetworkSwitch} from "./flow-provider-wrapper" import {useState, useRef, useEffect} from "react" // Standard networks supported by the demo type DemoFlowNetwork = "emulator" | "testnet" | "mainnet" interface HeaderProps { onMobileSidebarToggle?: () => void mobileSidebarOpen?: boolean } export function Header({ onMobileSidebarToggle, mobileSidebarOpen, }: HeaderProps = {}) { const {darkMode, toggleDarkMode} = useDarkMode() const {currentNetwork, switchNetwork, isNetworkSwitching} = useNetworkSwitch() const [networkMenuOpen, setNetworkMenuOpen] = useState(false) const networkMenuRef = useRef(null) const {data: chainId, isLoading: isChainIdLoading} = useFlowChainId() const getNetworkInfo = (network?: DemoFlowNetwork) => { const targetNetwork = network || currentNetwork if (isChainIdLoading && !network) return {name: "...", color: "gray"} switch (targetNetwork) { case "mainnet": return {name: "Mainnet", color: "green"} case "testnet": return {name: "Testnet", color: "blue"} case "emulator": return {name: "Emulator", color: "orange"} default: return {name: "Unknown", color: "gray"} } } const networkInfo = getNetworkInfo() // Only allow switching to emulator if app was started with emulator configuration const startedWithEmulator = (import.meta.env.VITE_FLOW_NETWORK as DemoFlowNetwork) === "emulator" const networks: DemoFlowNetwork[] = startedWithEmulator ? ["mainnet", "testnet", "emulator"] : ["mainnet", "testnet"] const handleNetworkSwitch = async (network: DemoFlowNetwork) => { if (network !== currentNetwork && !isNetworkSwitching) { setNetworkMenuOpen(false) await switchNetwork(network) } } // Close network menu when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( networkMenuRef.current && !networkMenuRef.current.contains(event.target as Node) ) { setNetworkMenuOpen(false) } } document.addEventListener("mousedown", handleClickOutside) return () => document.removeEventListener("mousedown", handleClickOutside) }, []) return (
Flow
Flow

React SDK

{networkMenuOpen && (
{networks.map(network => { const info = getNetworkInfo(network) const isActive = network === currentNetwork return ( ) })}
)}
) } ================================================ FILE: packages/demo/src/components/hook-cards/use-cross-vm-bridge-nft-from-evm-card.tsx ================================================ import {useCrossVmBridgeNftFromEvm, useFlowConfig} from "@onflow/react-sdk" import {useState, useMemo} from "react" import {useDarkMode} from "../flow-provider-wrapper" import {DemoCard} from "../ui/demo-card" import {ResultsSection} from "../ui/results-section" import {getContractAddress} from "../../constants" import {PlusGridIcon} from "../ui/plus-grid" const IMPLEMENTATION_CODE = `import { useCrossVmBridgeNftFromEvm } from "@onflow/react-sdk" const { crossVmBridgeNftFromEvm, isPending, error, data: txId } = useCrossVmBridgeNftFromEvm() crossVmBridgeNftFromEvm({ nftIdentifier: "A.dfc20aee650fcbdf.ExampleNFT.NFT", nftId: "1" })` export function UseCrossVmBridgeNftFromEvmCard() { const {darkMode} = useDarkMode() const config = useFlowConfig() const currentNetwork = config.flowNetwork || "emulator" const [nftIdentifier, setNftIdentifier] = useState("") const [nftId, setNftId] = useState("1") const { crossVmBridgeNftFromEvm, isPending, data: transactionId, error, } = useCrossVmBridgeNftFromEvm() const exampleNftData = useMemo(() => { if (currentNetwork !== "testnet") return null const exampleNftAddress = getContractAddress("ExampleNFT", currentNetwork) return { name: "Example NFT", nftIdentifier: `A.${exampleNftAddress.replace("0x", "")}.ExampleNFT.NFT`, nftId: "1", } }, [currentNetwork]) // Set default NFT identifier when network changes to testnet useMemo(() => { if (exampleNftData && !nftIdentifier) { setNftIdentifier(exampleNftData.nftIdentifier) } }, [exampleNftData, nftIdentifier]) const handleBridgeNft = () => { crossVmBridgeNftFromEvm({ nftIdentifier, nftId, }) } return (
{exampleNftData && (

Note: Example prefilled with ExampleNFT type identifier for testnet

)}
setNftIdentifier(e.target.value)} placeholder={ exampleNftData ? exampleNftData.nftIdentifier : "e.g., A.dfc20aee650fcbdf.ExampleNFT.NFT" } className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ darkMode ? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50` : `bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50` } outline-none`} />
setNftId(e.target.value)} placeholder="e.g., 1" className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ darkMode ? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50` : `bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50` } outline-none`} />
) } ================================================ FILE: packages/demo/src/components/hook-cards/use-cross-vm-bridge-nft-to-evm-card.tsx ================================================ import {useCrossVmBridgeNftToEvm, useFlowConfig} from "@onflow/react-sdk" import {useState, useMemo} from "react" import {useDarkMode} from "../flow-provider-wrapper" import {DemoCard} from "../ui/demo-card" import {ResultsSection} from "../ui/results-section" import {getContractAddress} from "../../constants" import {PlusGridIcon} from "../ui/plus-grid" const IMPLEMENTATION_CODE = `import { useCrossVmBridgeNftToEvm } from "@onflow/react-sdk" const { crossVmBridgeNftToEvm, isPending, error, data: txId } = useCrossVmBridgeNftToEvm() crossVmBridgeNftToEvm({ nftIdentifier: "A.012e4d204a60ac6f.ExampleNFT.NFT", nftIds: ["1", "2", "3"], calls: [] })` export function UseCrossVmBridgeNftToEvmCard() { const {darkMode} = useDarkMode() const config = useFlowConfig() const currentNetwork = config.flowNetwork || "emulator" const [nftIdentifier, setNftIdentifier] = useState("") const [nftIds, setNftIds] = useState("1") const { crossVmBridgeNftToEvm, isPending, data: transactionId, error, } = useCrossVmBridgeNftToEvm() const exampleNftData = useMemo(() => { if (currentNetwork !== "testnet") return null const exampleNftAddress = getContractAddress("ExampleNFT", currentNetwork) return { name: "Example NFT", nftIdentifier: `A.${exampleNftAddress.replace("0x", "")}.ExampleNFT.NFT`, nftIds: "1", } }, [currentNetwork]) // Set default NFT identifier when network changes to testnet useMemo(() => { if (exampleNftData && !nftIdentifier) { setNftIdentifier(exampleNftData.nftIdentifier) setNftIds(exampleNftData.nftIds) } }, [exampleNftData, nftIdentifier]) const handleBridgeNft = () => { const nftIdArray = nftIds.split(",").map(id => id.trim()) crossVmBridgeNftToEvm({ nftIdentifier, nftIds: nftIdArray, calls: [], // No EVM calls, just bridging }) } return (
{exampleNftData && (

Note: Example prefilled with ExampleNFT type identifier for testnet

)}
setNftIdentifier(e.target.value)} placeholder={ exampleNftData ? exampleNftData.nftIdentifier : "e.g., A.012e4d204a60ac6f.ExampleNFT.NFT" } className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ darkMode ? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50` : `bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50` } outline-none`} />
setNftIds(e.target.value)} placeholder="e.g., 1,2,3" className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ darkMode ? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50` : `bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50` } outline-none`} />
) } ================================================ FILE: packages/demo/src/components/hook-cards/use-cross-vm-bridge-token-from-evm-card.tsx ================================================ import {useCrossVmBridgeTokenFromEvm, useFlowConfig} from "@onflow/react-sdk" import {useState, useMemo} from "react" import {useDarkMode} from "../flow-provider-wrapper" import {DemoCard} from "../ui/demo-card" import {ResultsSection} from "../ui/results-section" import {getContractAddress} from "../../constants" import {PlusGridIcon} from "../ui/plus-grid" const IMPLEMENTATION_CODE = `import { useCrossVmBridgeTokenFromEvm } from "@onflow/react-sdk" const { crossVmBridgeTokenFromEvm, isPending, error, data: txId } = useCrossVmBridgeTokenFromEvm() crossVmBridgeTokenFromEvm({ vaultIdentifier: "A.dfc20aee650fcbdf.EVMVMBridgedToken_xxx.Vault", amount: "1000000000000000000" })` export function UseCrossVmBridgeTokenFromEvmCard() { const {darkMode} = useDarkMode() const config = useFlowConfig() const currentNetwork = config.flowNetwork || "emulator" const [vaultIdentifier, setVaultIdentifier] = useState("") const [amount, setAmount] = useState("1") // User-friendly amount const [decimals, setDecimals] = useState("18") // ERC20 decimals const { crossVmBridgeTokenFromEvm, isPending, data: transactionId, error, } = useCrossVmBridgeTokenFromEvm() const clickTokenData = useMemo(() => { if (currentNetwork !== "testnet") return null const clickTokenAddress = getContractAddress("ClickToken", currentNetwork) return { name: "ClickToken", vaultIdentifier: `A.${clickTokenAddress.replace("0x", "")}.EVMVMBridgedToken_a7cf2260e501952c71189d04fad17c704dfb36e6.Vault`, amount: "1", // 1 ClickToken decimals: "18", // ClickToken has 18 decimals } }, [currentNetwork]) // Set default vault identifier when network changes to testnet useMemo(() => { if (clickTokenData && !vaultIdentifier) { setVaultIdentifier(clickTokenData.vaultIdentifier) setAmount(clickTokenData.amount) setDecimals(clickTokenData.decimals) } }, [clickTokenData, vaultIdentifier]) // Convert user-friendly amount to Wei (UInt256) const convertToWei = (value: string, tokenDecimals: string): string => { try { // Remove any non-numeric characters except decimal point const cleaned = value.replace(/[^\d.]/g, "") if (!cleaned || cleaned === ".") return "0" const decimalPlaces = parseInt(tokenDecimals) || 18 const [whole = "0", fraction = ""] = cleaned.split(".") // Pad or truncate fraction to match decimals const paddedFraction = fraction .padEnd(decimalPlaces, "0") .slice(0, decimalPlaces) // Combine whole and fraction parts const weiValue = (whole || "0") + paddedFraction // Remove leading zeros but keep at least one digit const result = weiValue.replace(/^0+/, "") || "0" return result } catch { return "0" } } const handleBridgeToken = () => { const weiAmount = convertToWei(amount, decimals) console.log("Bridging token:", { amount, decimals, weiAmount, vaultIdentifier, }) crossVmBridgeTokenFromEvm({ vaultIdentifier, amount: weiAmount, }) } return (
{clickTokenData && (

Note: Example prefilled with ClickToken (ERC20) vault identifier for testnet

)}
setVaultIdentifier(e.target.value)} placeholder={ clickTokenData ? clickTokenData.vaultIdentifier : "e.g., A.dfc20aee650fcbdf.EVMVMBridgedToken_xxx.Vault" } className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ darkMode ? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50` : `bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50` } outline-none`} />
setAmount(e.target.value)} placeholder="e.g., 1 or 1.5" className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ darkMode ? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50` : `bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50` } outline-none`} />
setDecimals(e.target.value)} placeholder="e.g., 18" className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ darkMode ? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50` : `bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50` } outline-none`} />

ERC20 decimals (typically 18). Amount will be converted to Wei automatically.

) } ================================================ FILE: packages/demo/src/components/hook-cards/use-cross-vm-bridge-token-to-evm-card.tsx ================================================ import {useCrossVmBridgeTokenToEvm, useFlowConfig} from "@onflow/react-sdk" import {useState, useMemo} from "react" import {useDarkMode} from "../flow-provider-wrapper" import {DemoCard} from "../ui/demo-card" import {ResultsSection} from "../ui/results-section" import {getContractAddress} from "../../constants" import {PlusGridIcon} from "../ui/plus-grid" const IMPLEMENTATION_CODE = `import { useCrossVmBridgeTokenToEvm } from "@onflow/react-sdk" const { crossVmBridgeTokenToEvm, isPending, error, data: txId } = useCrossVmBridgeTokenToEvm() crossVmBridgeTokenToEvm({ vaultIdentifier: "A.dfc20aee650fcbdf.EVMVMBridgedToken_xxx.Vault", amount: "1.0", calls: [] })` export function UseCrossVmBridgeTokenToEvmCard() { const {darkMode} = useDarkMode() const config = useFlowConfig() const currentNetwork = config.flowNetwork || "emulator" const [vaultIdentifier, setVaultIdentifier] = useState("") const [amount, setAmount] = useState("1") // UFix64 in Cadence (8 decimals) const { crossVmBridgeTokenToEvm, isPending, data: transactionId, error, } = useCrossVmBridgeTokenToEvm() const clickTokenData = useMemo(() => { if (currentNetwork !== "testnet") return null const clickTokenAddress = getContractAddress("ClickToken", currentNetwork) return { name: "ClickToken", vaultIdentifier: `A.${clickTokenAddress.replace("0x", "")}.EVMVMBridgedToken_a7cf2260e501952c71189d04fad17c704dfb36e6.Vault`, amount: "1", // 1 ClickToken (UFix64 with 8 decimals) } }, [currentNetwork]) // Set default vault identifier when network changes to testnet useMemo(() => { if (clickTokenData && !vaultIdentifier) { setVaultIdentifier(clickTokenData.vaultIdentifier) setAmount(clickTokenData.amount) } }, [clickTokenData, vaultIdentifier]) // Normalize amount to ensure it has a decimal point for UFix64 const normalizeAmount = (value: string): string => { // Remove any non-numeric characters except decimal point const cleaned = value.replace(/[^\d.]/g, "") // If it's just a number without decimal, add .0 if (cleaned && !cleaned.includes(".")) { return `${cleaned}.0` } return cleaned } const handleBridgeToken = () => { const normalizedAmount = normalizeAmount(amount) crossVmBridgeTokenToEvm({ vaultIdentifier, amount: normalizedAmount, calls: [], // No EVM calls, just bridging }) } return (
{clickTokenData && (

Note: Example prefilled with ClickToken (ERC20) vault identifier for testnet

)}
setVaultIdentifier(e.target.value)} placeholder={ clickTokenData ? clickTokenData.vaultIdentifier : "e.g., A.dfc20aee650fcbdf.EVMVMBridgedToken_xxx.Vault" } className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ darkMode ? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50` : `bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50` } outline-none`} />
setAmount(e.target.value)} placeholder="e.g., 1 or 1.5" className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ darkMode ? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50` : `bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50` } outline-none`} />

Enter amount as a number (e.g., "1" or "1.5"). Will be converted to UFix64 format.

) } ================================================ FILE: packages/demo/src/components/hook-cards/use-flow-account-card.tsx ================================================ import {useState} from "react" import {useFlowAccount} from "@onflow/react-sdk" import {DemoCard} from "../ui/demo-card" import {useDarkMode} from "../flow-provider-wrapper" import {PlusGridIcon} from "../ui/plus-grid" import {ResultsSection} from "../ui/results-section" const IMPLEMENTATION_CODE = `import { useFlowAccount } from "@onflow/react-sdk" const { data: account, isLoading, error, refetch, } = useFlowAccount({ address, query: { enabled: true }, })` export function UseFlowAccountCard() { const {darkMode} = useDarkMode() const [address, setAddress] = useState("") const { data: account, isLoading, error, refetch, } = useFlowAccount({ address, query: {enabled: !!address}, }) return (
setAddress(e.target.value)} placeholder="Enter Flow address (e.g., 0x55ad22f01ef568a1)" className={`w-full px-4 py-3 font-mono text-sm rounded-lg border outline-none transition-all duration-200 ${ darkMode ? `bg-gray-900 text-white border-white/10 focus:border-flow-primary placeholder-gray-600` : `bg-white text-black border-black/10 focus:border-flow-primary placeholder-gray-400` }`} />
{account && !error && (

Balance

{(Number(account.balance) / 1e8).toFixed(8)}

FLOW

Account Keys

{account.keys.length}

{account.keys.length === 1 ? "key" : "keys"}

)}
) } ================================================ FILE: packages/demo/src/components/hook-cards/use-flow-block-card.tsx ================================================ import {useState} from "react" import {useFlowBlock} from "@onflow/react-sdk" import {DemoCard} from "../ui/demo-card" import {useDarkMode} from "../flow-provider-wrapper" import {PlusGridIcon} from "../ui/plus-grid" import {ResultsSection} from "../ui/results-section" const IMPLEMENTATION_CODE = `import { useFlowBlock } from "@onflow/react-sdk" const { data: block, isLoading, error, refetch } = useFlowBlock({ query: { enabled: true } })` export function UseFlowBlockCard() { const {darkMode} = useDarkMode() const [blockHeight, setBlockHeight] = useState("") const [blockId, setBlockId] = useState("") const [useLatest, setUseLatest] = useState(true) const { data: block, isLoading, error, refetch, } = useFlowBlock({ height: useLatest ? undefined : blockHeight ? parseInt(blockHeight) : undefined, id: useLatest ? undefined : blockId || undefined, query: {enabled: false, staleTime: 30000}, }) return (
{!block && !isLoading && !error && (

{useLatest ? "Click to fetch latest block" : "Configure and fetch block data"}

)}
{!useLatest && (
setBlockHeight(e.target.value)} placeholder="Block height (e.g., 12345)" className={`w-full px-4 py-3 font-mono text-sm rounded-lg border outline-none transition-all duration-200 ${ darkMode ? `bg-gray-900 text-white border-white/10 focus:border-flow-primary placeholder-gray-600` : `bg-white text-black border-black/10 focus:border-flow-primary placeholder-gray-400` }`} />
setBlockId(e.target.value)} placeholder="Block ID (optional)" className={`w-full px-4 py-3 font-mono text-sm rounded-lg border outline-none transition-all duration-200 ${ darkMode ? `bg-gray-900 text-white border-white/10 focus:border-flow-primary placeholder-gray-600` : `bg-white text-black border-black/10 focus:border-flow-primary placeholder-gray-400` }`} />
)}
{error ? (

{error.message}

) : block ? (

Height

{block.height}

Timestamp

{new Date(block.timestamp).toLocaleDateString()}

{new Date(block.timestamp).toLocaleTimeString()}

Block ID

{block.id}

Parent Block ID

{block.parentId}

) : null}
) } ================================================ FILE: packages/demo/src/components/hook-cards/use-flow-chain-id-card.tsx ================================================ import {useFlowChainId} from "@onflow/react-sdk" import {DemoCard} from "../ui/demo-card" import {useDarkMode} from "../flow-provider-wrapper" import {PlusGridIcon} from "../ui/plus-grid" import {ResultsSection} from "../ui/results-section" import { getNetworkName, getEVMChainId, normalizeChainId, isProductionNetwork, } from "../../utils/chain-helpers" const IMPLEMENTATION_CODE = `import { useFlowChainId } from "@onflow/react-sdk" const { data: chainId, isLoading } = useFlowChainId()` export function UseFlowChainIdCard() { const {darkMode} = useDarkMode() const { data: chainId, isLoading, error, refetch, } = useFlowChainId({ query: {enabled: false, staleTime: 300000}, // 5 minutes cache }) const getChainColor = (id: string) => { const normalized = normalizeChainId(id) switch (normalized) { case "mainnet": return "bg-[#00EF8B]" case "testnet": return "bg-[#FFB800]" case "emulator": return "bg-[#9945FF]" default: // Handle legacy canarynet or other unknown formats if (id === "flow-canarynet") return "bg-[#FF6B6B]" return "bg-gray-500" } } return (

Chain Information

{isLoading && (

Loading chain ID...

)} {error && (

Error: {error.message}

)} {chainId && !isLoading && !error && (
{getNetworkName(chainId)} {isProductionNetwork(chainId) && ( PROD )}
{chainId}
{getEVMChainId(chainId) && (
EVM Chain ID: {getEVMChainId(chainId)}
)}
)} {!chainId && !isLoading && !error && (

Click "Fetch Chain ID" to load current chain information

)}
) } ================================================ FILE: packages/demo/src/components/hook-cards/use-flow-config-card.tsx ================================================ import {useFlowConfig} from "@onflow/react-sdk" import {DemoCard} from "../ui/demo-card" import {useDarkMode} from "../flow-provider-wrapper" import {PlusGridIcon} from "../ui/plus-grid" import {ResultsSection} from "../ui/results-section" const IMPLEMENTATION_CODE = `import { useFlowConfig } from "@onflow/react-sdk" const config = useFlowConfig()` export function UseFlowConfigCard() { const {darkMode} = useDarkMode() const config = useFlowConfig() return (

Flow Configuration

{config ? (
                  {JSON.stringify(config, null, 2)}
                
) : (

Loading configuration...

)}
{config && (
Network

{config.flowNetwork || "Not specified"}

Access Node

{config["accessNode.api"] || "Not configured"}

)}
) } ================================================ FILE: packages/demo/src/components/hook-cards/use-flow-current-user-card.tsx ================================================ import {useFlowCurrentUser} from "@onflow/react-sdk" import {DemoCard} from "../ui/demo-card" import {useDarkMode} from "../flow-provider-wrapper" import {PlusGridIcon} from "../ui/plus-grid" import {ResultsSection} from "../ui/results-section" const IMPLEMENTATION_CODE = `import { useFlowCurrentUser } from "@onflow/react-sdk" const { user, authenticate, unauthenticate } = useFlowCurrentUser()` export function UseFlowCurrentUserCard() { const {darkMode} = useDarkMode() const {user, authenticate, unauthenticate} = useFlowCurrentUser() return (

Authentication Status

{user?.loggedIn ? "Connected" : "Not Connected"}
{user?.loggedIn ? (

Wallet Address

{user.addr}

) : (

No wallet connected

)}
{!user?.loggedIn ? ( ) : ( )}
) } ================================================ FILE: packages/demo/src/components/hook-cards/use-flow-events-card.tsx ================================================ import {useFlowEvents, useFlowConfig} from "@onflow/react-sdk" import {useState, useCallback} from "react" import type {Event} from "@onflow/typedefs" import {getEventType} from "../../constants" import {DemoCard} from "../ui/demo-card" import {useDarkMode} from "../flow-provider-wrapper" import {PlusGridIcon} from "../ui/plus-grid" import {ResultsSection} from "../ui/results-section" const IMPLEMENTATION_CODE = `import { useFlowEvents } from "@onflow/react-sdk" useFlowEvents({ eventType: "A.1654653399040a61.FlowToken.TokensWithdrawn", startBlockHeight: 0, onEvent: (event) => { console.log("New event:", event) }, onError: (error) => { console.error("Event error:", error) } })` export function UseFlowEventsCard() { const {darkMode} = useDarkMode() const config = useFlowConfig() const currentNetwork = config.flowNetwork || "emulator" const [eventTypes, setEventTypes] = useState([ getEventType("FlowToken", "TokensWithdrawn", currentNetwork), ]) const [fromBlock, setFromBlock] = useState("") const [isListening, setIsListening] = useState(false) const [events, setEvents] = useState([]) const [error, setError] = useState(null) const onEvent = useCallback( (event: Event) => { if (isListening) { setEvents(prev => [event, ...prev].slice(0, 50)) // Keep last 50 events } }, [isListening] ) const onError = useCallback((err: Error) => { setError(err) setIsListening(false) }, []) // Always use the hook, but pass empty eventTypes when not listening useFlowEvents({ eventTypes: isListening ? eventTypes : [], startHeight: isListening && fromBlock ? parseInt(fromBlock) : undefined, onEvent, onError, }) const presetEvents = [ { name: "FlowToken Withdrawn", type: getEventType("FlowToken", "TokensWithdrawn", currentNetwork), }, { name: "FlowToken Deposited", type: getEventType("FlowToken", "TokensDeposited", currentNetwork), }, ] const handleStartListening = () => { setEvents([]) setError(null) setIsListening(true) } const handleStopListening = () => { setIsListening(false) } const handleAddEventType = (type: string) => { if (!eventTypes.includes(type)) { setEventTypes(prev => [...prev, type]) } } const handleRemoveEventType = (type: string) => { setEventTypes(prev => prev.filter(t => t !== type)) } return (
{presetEvents.map(preset => ( ))}
{eventTypes.map(type => (
{type}
))}
setFromBlock(e.target.value)} placeholder="Start block height" disabled={isListening} className={`w-full px-4 py-3 font-mono text-sm rounded-lg border outline-none transition-all duration-200 ${ isListening ? darkMode ? "bg-gray-800 text-gray-500 border-gray-700 cursor-not-allowed" : "bg-gray-100 text-gray-500 border-gray-300 cursor-not-allowed" : darkMode ? `bg-gray-900 text-white border-white/10 focus:border-flow-primary placeholder-gray-600` : `bg-white text-black border-black/10 focus:border-flow-primary placeholder-gray-400` }`} />

Events

{error && (

Error: {error.message}

)} {isListening && !error && (
Listening for events...
)} {!isListening && events.length === 0 && !error && (

No events received yet. Click "Start Listening" to begin.

)} {events.length > 0 && (
{events.map((event, index) => (
{event.type}
Block: {event.blockHeight} • Event #{event.eventIndex}
#{index + 1}
Show Details
                          {JSON.stringify(event, null, 2)}
                        
))}
)}
0} title="Events Received" />
) } ================================================ FILE: packages/demo/src/components/hook-cards/use-flow-mutate-card.tsx ================================================ import {useFlowConfig, useFlowMutate} from "@onflow/react-sdk" import {useState} from "react" import {useDarkMode} from "../flow-provider-wrapper" import {DemoCard} from "../ui/demo-card" import {ResultsSection} from "../ui/results-section" import {CodeEditor} from "../ui/code-editor" const IMPLEMENTATION_CODE = `import { useFlowMutate } from "@onflow/react-sdk" const { mutate, isPending, error, data: txId } = useFlowMutate({ cadence: TRANSACTION_CADENCE, args: (arg, t) => [arg("value", t.String)], limit: 999, })` export function UseFlowMutateCard() { const {darkMode} = useDarkMode() const config = useFlowConfig() const currentNetwork = config.flowNetwork || "emulator" const [cadenceTransaction, setCadenceTransaction] = useState( `transaction { prepare(signer: &Account) { log("Hello from a transaction!") } execute { log("Transaction executed successfully") } }`.trim() ) const {mutate, isPending, data: transactionId, error} = useFlowMutate() const presetTransactions = [ { name: "Simple Log", transaction: `transaction { prepare(signer: &Account) { log("Hello from a transaction!") } execute { log("Transaction executed successfully") } }`, }, ] const handleSendTransaction = () => { mutate({ cadence: cadenceTransaction, args: () => [], }) } return (
{presetTransactions.map(preset => ( ))}
) } ================================================ FILE: packages/demo/src/components/hook-cards/use-flow-nft-metadata-card.tsx ================================================ import {useState} from "react" import {useFlowNftMetadata} from "@onflow/react-sdk" import {DemoCard} from "../ui/demo-card" import {useDarkMode} from "../flow-provider-wrapper" import {PlusGridIcon} from "../ui/plus-grid" import {ResultsSection} from "../ui/results-section" const IMPLEMENTATION_CODE = `import { useFlowNftMetadata } from "@onflow/react-sdk" const { data: nft, isLoading, error, refetch, } = useFlowNftMetadata({ accountAddress, tokenId, publicPathIdentifier, query: { enabled: false, staleTime: 30000 }, })` export function UseFlowNftMetadataCard() { const {darkMode} = useDarkMode() const [accountAddress, setAccountAddress] = useState("") const [tokenId, setTokenId] = useState("") const [publicPathIdentifier, setPublicPathIdentifier] = useState( "exampleNFTCollection" ) // Normalize address to ensure it has 0x prefix const normalizedAddress = accountAddress ? accountAddress.startsWith("0x") ? accountAddress : `0x${accountAddress}` : undefined const { data: nft, isLoading, error, refetch, } = useFlowNftMetadata({ accountAddress: normalizedAddress, tokenId: tokenId || undefined, publicPathIdentifier: publicPathIdentifier || undefined, query: {enabled: false, staleTime: 30000}, }) return (
setAccountAddress(e.target.value)} placeholder="Account Address (0x...)" className={`w-full px-4 py-3 font-mono text-sm rounded-lg border outline-none transition-all duration-200 ${ darkMode ? `bg-gray-900 text-white border-white/10 focus:border-flow-primary placeholder-gray-600` : `bg-white text-black border-black/10 focus:border-flow-primary placeholder-gray-400` }`} /> setTokenId(e.target.value)} placeholder="Token ID (e.g., 123)" className={`w-full px-4 py-3 font-mono text-sm rounded-lg border outline-none transition-all duration-200 ${ darkMode ? `bg-gray-900 text-white border-white/10 focus:border-flow-primary placeholder-gray-600` : `bg-white text-black border-black/10 focus:border-flow-primary placeholder-gray-400` }`} /> setPublicPathIdentifier(e.target.value)} placeholder="Public Path Identifier" className={`w-full px-4 py-3 font-mono text-sm rounded-lg border outline-none transition-all duration-200 ${ darkMode ? `bg-gray-900 text-white border-white/10 focus:border-flow-primary placeholder-gray-600` : `bg-white text-black border-black/10 focus:border-flow-primary placeholder-gray-400` }`} />
{nft && !error && typeof nft === "object" && (
{nft.thumbnailUrl && (

NFT Image

{nft.name} { const target = e.target as HTMLImageElement target.style.display = "none" }} />
)}
{!nft.thumbnailUrl && ( )}

Name

{nft.name}

{nft.description && (

Description

{nft.description}

)}

Token ID

{nft.tokenID}

{nft.collectionName && (

Collection

{nft.collectionName}

)}
{nft.traits && Object.keys(nft.traits).length > 0 && (

Traits

{Object.entries(nft.traits).map(([key, value]) => (

{key}

{String(value)}

))}
)}
)} {!isLoading && !nft && !error && accountAddress && tokenId && (

No NFT found. This could mean:

  • The NFT doesn't exist at this address/token ID
  • The public path identifier is incorrect for this NFT collection
  • Try common paths: "exampleNFTCollection", "TopShotCollection", "FindMarketSaleCollection"
)}
) } ================================================ FILE: packages/demo/src/components/hook-cards/use-flow-query-card.tsx ================================================ import * as fcl from "@onflow/fcl" import { useFlowConfig, useFlowCurrentUser, useFlowQuery, } from "@onflow/react-sdk" import {useState} from "react" import {getContractAddress} from "../../constants" import {useDarkMode} from "../flow-provider-wrapper" import {DemoCard} from "../ui/demo-card" import {ResultsSection} from "../ui/results-section" import {CodeEditor} from "../ui/code-editor" const IMPLEMENTATION_CODE = `import { useFlowQuery } from "@onflow/react-sdk" const { data, isLoading, error, refetch } = useFlowQuery({ cadence: \` access(all) fun main(): String { return "Hello, Flow!" } \`, args: [], })` export function UseFlowQueryCard() { const {darkMode} = useDarkMode() const config = useFlowConfig() const {user: currentUser} = useFlowCurrentUser() const currentNetwork = config.flowNetwork || "emulator" const [cadenceScript, setCadenceScript] = useState( `access(all) fun main(): String { return "Hello, World!" }`.trim() ) const [args, setArgs] = useState< (arg: typeof fcl.arg, t: typeof fcl.t) => any[] >(() => () => []) const { data: result, isLoading, error, refetch, } = useFlowQuery({ cadence: cadenceScript, args, query: {enabled: false, staleTime: 10000}, }) const presetScripts = [ { name: "Hello World", script: `access(all) fun main(): String { return "Hello, World!" }`, args: () => () => [], }, { name: "Current Block Height", script: `access(all) fun main(): UInt64 { return getCurrentBlock().height }`, args: () => () => [], }, { name: "Get Account Balance", script: `import FlowToken from ${getContractAddress( "FlowToken", currentNetwork )} access(all) fun main(address: Address): UFix64 { let account = getAccount(address) let vaultRef = account.capabilities.borrow<&FlowToken.Vault>(/public/flowTokenBalance) return vaultRef?.balance ?? 0.0 }`, args: () => { if (!currentUser?.addr) { alert("Please connect your wallet to run this script.") return null } return () => [fcl.arg(currentUser.addr, fcl.t.Address)] }, }, ] const onSelectPreset = (preset: (typeof presetScripts)[number]) => { const newArgs = preset.args() if (newArgs) { setCadenceScript(preset.script) setArgs(() => newArgs) } } return (
{presetScripts.map(preset => ( ))}
) } ================================================ FILE: packages/demo/src/components/hook-cards/use-flow-query-raw-card.tsx ================================================ import { useFlowConfig, useFlowCurrentUser, useFlowQueryRaw, } from "@onflow/react-sdk" import {useState} from "react" import * as fcl from "@onflow/fcl" import {getContractAddress} from "../../constants" import {DemoCard} from "../ui/demo-card" import {useDarkMode} from "../flow-provider-wrapper" import {PlusGridIcon} from "../ui/plus-grid" import {ResultsSection} from "../ui/results-section" import {CodeEditor} from "../ui/code-editor" const IMPLEMENTATION_CODE = `import { useFlowQueryRaw } from "@onflow/react-sdk" const { data, isLoading, error, refetch } = useFlowQueryRaw({ cadence: \` access(all) fun main(): String { return "Hello, Flow!" } \`, args: [], })` export function UseFlowQueryRawCard() { const {darkMode} = useDarkMode() const config = useFlowConfig() const {user: currentUser} = useFlowCurrentUser() const currentNetwork = config.flowNetwork || "emulator" const [cadenceScript, setCadenceScript] = useState( `access(all) fun main(): String { return "Hello from Raw Query!" }`.trim() ) const [args, setArgs] = useState< (arg: typeof fcl.arg, t: typeof fcl.t) => any[] >(() => () => []) const { data: result, isLoading, error, refetch, } = useFlowQueryRaw({ cadence: cadenceScript, args, query: {enabled: false, staleTime: 10000}, }) const presetScripts = [ { name: "Hello World", script: `access(all) fun main(): String { return "Hello from Raw Query!" }`, args: () => () => [], }, { name: "Current Block Info", script: `access(all) fun main(): [AnyStruct] { let block = getCurrentBlock() return [block.height, block.id, block.timestamp] }`, args: () => () => [], }, { name: "Get Account Balance", script: `import FlowToken from ${getContractAddress( "FlowToken", currentNetwork )} access(all) fun main(address: Address): UFix64 { let account = getAccount(address) let vaultRef = account.capabilities.borrow<&FlowToken.Vault>(/public/flowTokenBalance) return vaultRef?.balance ?? 0.0 }`, args: () => { if (!currentUser?.addr) { alert("Please connect your wallet to run this script.") return null } return () => [fcl.arg(currentUser.addr, fcl.t.Address)] }, }, ] const onSelectPreset = (preset: (typeof presetScripts)[number]) => { const newArgs = preset.args() if (newArgs) { setCadenceScript(preset.script) setArgs(() => newArgs) } } return (

Raw Query Hook

Returns the complete FCL response without automatic parsing, giving you access to status, events, and raw data.

{presetScripts.map(preset => ( ))}
) } ================================================ FILE: packages/demo/src/components/hook-cards/use-flow-revertible-random-card.tsx ================================================ import {useFlowRevertibleRandom} from "@onflow/react-sdk" import {DemoCard} from "../ui/demo-card" import {useDarkMode} from "../flow-provider-wrapper" import {PlusGridIcon} from "../ui/plus-grid" import {ResultsSection} from "../ui/results-section" const IMPLEMENTATION_CODE = `import { useFlowRevertibleRandom } from "@onflow/react-sdk" const { data: randomNumber, isLoading, error, refetch } = useFlowRevertibleRandom({ query: { enabled: true } })` export function UseFlowRevertibleRandomCard() { const {darkMode} = useDarkMode() const { data: randomResults, isLoading, error, refetch, } = useFlowRevertibleRandom({ max: "1000000000", count: 1, }) return ( Generate pseudorandom numbers for simple use cases like randomized UIs. Values are deterministic within the same block.{" "} See advanced patterns {" "} for onchain randomness. } code={IMPLEMENTATION_CODE} docsUrl="https://developers.flow.com/build/tools/react-sdk/hooks#useflowrevertiblerandom" >
{randomResults && randomResults.length > 0 && !isLoading && !error && (

Generated Random Value

{randomResults[0].value}
Hexadecimal
0x {BigInt(randomResults[0].value).toString(16).toUpperCase()}
Block Height
{randomResults[0].blockHeight}
)}
{error && (

Generation Failed

{error.message}

)} 0 && !error} />
) } ================================================ FILE: packages/demo/src/components/hook-cards/use-flow-scheduled-transaction-card.tsx ================================================ import { useFlowCurrentUser, useFlowScheduledTransactionList, useFlowScheduledTransaction, useFlowScheduledTransactionSetup, useFlowScheduledTransactionCancel, ScheduledTransactionPriority, ScheduledTransactionStatus, type ScheduledTransaction, } from "@onflow/react-sdk" import {useState} from "react" import {useDarkMode} from "../flow-provider-wrapper" import {DemoCard} from "../ui/demo-card" import {ResultsSection} from "../ui/results-section" const IMPLEMENTATION_CODE = `import { useFlowScheduledTransactionList, useFlowScheduledTransaction, useFlowScheduledTransactionSetup, useFlowScheduledTransactionCancel } from "@onflow/react-sdk" // Setup the scheduler (one-time initialization) const { setupAsync, isPending: isSettingUp } = useFlowScheduledTransactionSetup() await setupAsync() // List all scheduled transactions for an account const { data: transactions, isLoading } = useFlowScheduledTransactionList({ account: "0xACCOUNT", includeHandlerData: true // Optional: include handler details }) // Get a specific scheduled transaction by ID const { data: transaction } = useFlowScheduledTransaction({ txId: "42", includeHandlerData: true }) // Cancel a scheduled transaction const { cancelTransactionAsync } = useFlowScheduledTransactionCancel() await cancelTransactionAsync("42")` const PRIORITY_LABELS: Record = { [ScheduledTransactionPriority.Low]: "Low", [ScheduledTransactionPriority.Medium]: "Medium", [ScheduledTransactionPriority.High]: "High", } const STATUS_LABELS: Record = { [ScheduledTransactionStatus.Pending]: "Pending", [ScheduledTransactionStatus.Processing]: "Processing", [ScheduledTransactionStatus.Completed]: "Completed", [ScheduledTransactionStatus.Failed]: "Failed", [ScheduledTransactionStatus.Cancelled]: "Cancelled", } export function UseFlowScheduledTransactionCard() { const {darkMode} = useDarkMode() const {user} = useFlowCurrentUser() // Individual hooks for each operation const {setupAsync, isPending: isSettingUp} = useFlowScheduledTransactionSetup() const {cancelTransactionAsync, isPending: isCancelling} = useFlowScheduledTransactionCancel() const [activeTab, setActiveTab] = useState< "setup" | "list" | "get" | "cancel" >("setup") const [txId, setTxId] = useState("") const [accountAddress, setAccountAddress] = useState("") const [includeHandlerData, setIncludeHandlerData] = useState(false) const [result, setResult] = useState(null) const [error, setError] = useState(null) // Helper function to normalize address (add 0x prefix if missing) const normalizeAddress = (address: string): string => { if (!address) return address const trimmed = address.trim() return trimmed.startsWith("0x") ? trimmed : `0x${trimmed}` } // Query hooks - reactive to input changes const normalizedAccountAddress = accountAddress ? normalizeAddress(accountAddress) : user?.addr const listQuery = useFlowScheduledTransactionList({ account: normalizedAccountAddress, includeHandlerData, query: { enabled: activeTab === "list" && Boolean(normalizedAccountAddress), }, }) const getQuery = useFlowScheduledTransaction({ txId: txId, includeHandlerData, query: { enabled: activeTab === "get" && Boolean(txId), }, }) const handleSetup = async () => { setError(null) setResult(null) try { const txId = await setupAsync() setResult({txId, message: "Manager setup successfully"}) } catch (err: any) { setError(err.message || "Setup failed") } } const handleList = () => { setError(null) setResult(null) if (!normalizedAccountAddress) { setError("Please connect your wallet or enter an account address") return } // Results will automatically update via listQuery if (listQuery.data) { setResult({ account: normalizedAccountAddress, count: listQuery.data.length, transactions: listQuery.data, }) } } const handleGet = () => { setError(null) setResult(null) if (!txId) { setError("Please enter a transaction ID") return } // Results will automatically update via getQuery if (getQuery.data !== undefined) { setResult(getQuery.data || {message: "Transaction not found"}) } } const handleCancel = async () => { if (!txId) { setError("Please enter a transaction ID") return } setError(null) setResult(null) try { const cancelTxId = await cancelTransactionAsync(txId) setResult({ txId: cancelTxId, message: "Transaction cancelled successfully", }) } catch (err: any) { setError(err.message || "Failed to cancel transaction") } } const formatTransactionInfo = (tx: ScheduledTransaction) => { if (!tx.id) return tx return { ID: tx.id, Priority: PRIORITY_LABELS[tx.priority as ScheduledTransactionPriority] || tx.priority, Status: STATUS_LABELS[tx.status as ScheduledTransactionStatus] || tx.status, "Execution Effort": tx.executionEffort.toString(), "Fees (FLOW)": tx.fees.formatted, "Scheduled At": new Date(tx.scheduledTimestamp * 1000).toLocaleString(), "Handler Type": tx.handlerTypeIdentifier, "Handler Address": tx.handlerAddress, ...(tx.handlerUUID && {"Handler UUID": tx.handlerUUID}), ...(tx.handlerResolvedViews && { "Handler Views": Object.keys(tx.handlerResolvedViews).length, }), } } const isLoading = (activeTab === "list" && listQuery.isLoading) || (activeTab === "get" && getQuery.isLoading) || isSettingUp || isCancelling return (
{(["setup", "list", "get", "cancel"] as const).map(tab => ( ))}
{activeTab === "setup" && (

Setup Scheduler Manager

Before scheduling transactions, you need to initialize a Manager resource in your account. This is a one-time setup that creates the necessary storage and capabilities for managing scheduled transactions. The manager tracks your scheduled transactions and handles their execution.

{!user?.addr && (

Please connect your wallet to setup the scheduler

)}
)} {activeTab === "list" && (

List Scheduled Transactions

View all scheduled transactions for an account. Each transaction shows its ID, priority level (High/Medium/Low), execution status, computational effort, fees, and scheduled execution time. Optionally include handler data to see the transaction's metadata and resolved views.

Note: Only returns transactions in pending state (not yet executed). Once executed, transactions are removed from the list.

setAccountAddress(e.target.value)} placeholder={ user?.addr ? `Default: ${user.addr}` : "e.g., 0x1234567890abcdef" } className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ darkMode ? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50` : `bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50` } outline-none`} />

Leave empty to use connected wallet address

setIncludeHandlerData(e.target.checked)} className="w-4 h-4" />
{listQuery.data && ( )}
)} {activeTab === "get" && (

Get Scheduled Transaction

Fetch detailed information about a specific scheduled transaction by its ID. This returns the transaction's current status, priority, fees, execution timestamp, and handler information. Useful for monitoring individual transactions and checking their execution state.

Note: Only returns data for transactions in pending state. Returns null if the transaction has been executed or doesn't exist.

setTxId(e.target.value)} placeholder="e.g., 123" className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ darkMode ? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50` : `bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50` } outline-none`} />
setIncludeHandlerData(e.target.checked)} className="w-4 h-4" />
{getQuery.data && ( )}
)} {activeTab === "cancel" && (

Cancel Scheduled Transaction

Cancel a pending scheduled transaction before it executes. This prevents the transaction from running and refunds any prepaid fees back to your account. Only transactions in "Pending" status can be cancelled - once a transaction starts processing or completes, it cannot be cancelled.

setTxId(e.target.value)} placeholder="e.g., 123" className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200 ${ darkMode ? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500 focus:border-flow-primary/50` : `bg-white border-black/10 text-black placeholder-gray-400 focus:border-flow-primary/50` } outline-none`} />
{!user?.addr && (

Please connect your wallet to cancel transactions

)}
)} {activeTab === "list" && listQuery.data && !result && ( )} {activeTab === "get" && getQuery.data && !result && ( )} {(result || error) && ( )} {isLoading && !result && !error && (
Loading...
)}
) } ================================================ FILE: packages/demo/src/components/hook-cards/use-flow-transaction-status-card.tsx ================================================ import {useFlowTransactionStatus} from "@onflow/react-sdk" import {useState, useEffect} from "react" import {DemoCard} from "../ui/demo-card" import {useDarkMode} from "../flow-provider-wrapper" import {PlusGridIcon} from "../ui/plus-grid" import {ResultsSection} from "../ui/results-section" const IMPLEMENTATION_CODE = `import { useFlowTransactionStatus } from "@onflow/react-sdk" const { transactionStatus, error } = useFlowTransactionStatus({ id: txId })` // Error boundary wrapper for the hook function useSafeFlowTransactionStatus(id?: string) { const [hookError, setHookError] = useState(null) const [transactionStatus, setTransactionStatus] = useState(null) const [isLoading, setIsLoading] = useState(false) // Direct hook call with error handling const hookResult = (() => { try { return useFlowTransactionStatus({ id, }) } catch (error) { return { transactionStatus: null, error: error instanceof Error ? error : new Error("Unknown error occurred"), } } })() useEffect(() => { if (!id) { setHookError(null) setTransactionStatus(null) setIsLoading(false) return } setIsLoading(true) setHookError(null) try { if (hookResult.error) { if (hookResult.error.message.includes("Invalid transactionId")) { setHookError("Invalid transaction ID format") } else { setHookError(hookResult.error.message) } } else { setTransactionStatus(hookResult.transactionStatus) } } catch (error) { setHookError( error instanceof Error ? error.message : "Failed to fetch transaction status" ) } finally { setIsLoading(false) } }, [id, hookResult.error, hookResult.transactionStatus]) return { transactionStatus, error: hookResult.error, hookError, isLoading, } } export function UseFlowTransactionStatusCard() { const {darkMode} = useDarkMode() const [transactionId, setTransactionId] = useState("") const [debugInfo, setDebugInfo] = useState("") // Normalize transaction ID - remove 0x prefix if present (Flow IDs don't use 0x) const normalizeTransactionId = (id: string): string => { const trimmedId = id.trim() if (!trimmedId) return "" // Remove 0x prefix if present if (trimmedId.startsWith("0x")) { return trimmedId.slice(2) } return trimmedId } // Flow transaction IDs are exactly 64 hex characters (no 0x prefix) const isValidTransactionId = (id: string): boolean => { const normalized = normalizeTransactionId(id) if (!normalized) return false // Must be exactly 64 hex characters if (normalized.length !== 64) { return false } // Must contain only valid hex characters return /^[a-fA-F0-9]{64}$/.test(normalized) } // Only pass valid transaction IDs to the hook, with error catching const validTransactionId = isValidTransactionId(transactionId) ? normalizeTransactionId(transactionId) : undefined // Add debug logging useEffect(() => { if (transactionId.trim()) { setDebugInfo( `Input: "${transactionId.trim()}", Length: ${transactionId.trim().length}, Valid: ${isValidTransactionId(transactionId)}` ) } else { setDebugInfo("") } }, [transactionId]) // Use safe wrapper hook const {transactionStatus, error, hookError, isLoading} = useSafeFlowTransactionStatus(validTransactionId) const getStatusColor = (statusCode: number) => { switch (statusCode) { case 0: // UNKNOWN return "bg-gray-500" case 1: // PENDING return "bg-yellow-500" case 2: // FINALIZED return "bg-purple-500" case 3: // EXECUTED return "bg-green-500" case 4: // SEALED return "bg-green-500" case 5: // EXPIRED return "bg-red-500" default: return "bg-gray-500" } } const getStatusText = (statusCode: number) => { switch (statusCode) { case 0: return "UNKNOWN" case 1: return "PENDING" case 2: return "FINALIZED" case 3: return "EXECUTED" case 4: return "SEALED" case 5: return "EXPIRED" default: return `UNKNOWN (${statusCode})` } } return (
setTransactionId(e.target.value)} placeholder="Enter Flow transaction ID (64 hex chars, 0x prefix optional)" className={`w-full px-4 py-3 font-mono text-sm rounded-lg border outline-none transition-all duration-200 ${ transactionId.trim() === "" ? darkMode ? `bg-gray-900 text-white border-white/10 focus:border-flow-primary placeholder-gray-600` : `bg-white text-black border-black/10 focus:border-flow-primary placeholder-gray-400` : isValidTransactionId(transactionId) ? darkMode ? "border-green-500 bg-green-900/20 text-green-400" : "border-green-500 bg-green-50 text-green-900" : darkMode ? "border-red-500 bg-red-900/20 text-red-400" : "border-red-500 bg-red-50 text-red-900" }`} /> {transactionId.trim() !== "" && !isValidTransactionId(transactionId) && (

Invalid Format

Flow transaction ID must be exactly 64 hex characters (0x prefix optional)

{debugInfo && (

{debugInfo}

)}
)} {transactionId.trim() !== "" && isValidTransactionId(transactionId) && (

Valid transaction ID format - monitoring status...

{debugInfo && (

{debugInfo}

)}
)} {!transactionId.trim() && (

Enter a transaction ID above to automatically monitor its status updates

)}

Status Legend

0

UNKNOWN

Transaction not found or not yet submitted

1

PENDING

Awaiting execution by network

2

FINALIZED

Included in a block by consensus nodes

3

EXECUTED

Execution completed, results computed

4

SEALED

Permanently committed to blockchain

5

EXPIRED

Transaction reference block expired

Transaction Status

{!transactionId.trim() && (

Enter a transaction ID to check its status

)} {transactionId.trim() !== "" && !isValidTransactionId(transactionId) && (

Please enter a valid transaction ID format to check status

)} {(error || hookError) && (

Status Check Failed

{hookError || error?.message}

)} {transactionStatus && !error && !hookError && (
{getStatusText(transactionStatus.status)} Status Code: {transactionStatus.status}
{transactionStatus.errorMessage && (

Transaction Error

{transactionStatus.errorMessage}

)}
)}
) } ================================================ FILE: packages/demo/src/components/setup-cards/installation-card.tsx ================================================ import {DemoCard} from "../ui/demo-card" import {useDarkMode} from "../flow-provider-wrapper" import {PlusGridIcon} from "../ui/plus-grid" import {InlineCode} from "../ui/inline-code" import {CodeViewer} from "../ui/code-viewer" import {useState} from "react" export function InstallationCard() { const {darkMode} = useDarkMode() const [copiedStates, setCopiedStates] = useState<{[key: string]: boolean}>({}) const copyToClipboard = async (text: string, key: string) => { try { await navigator.clipboard.writeText(text) setCopiedStates(prev => ({...prev, [key]: true})) setTimeout(() => { setCopiedStates(prev => ({...prev, [key]: false})) }, 2000) } catch (err) { console.error("Failed to copy text: ", err) } } return (
1

Install the package

yarn add @onflow/react-sdk pnpm add @onflow/react-sdk
2

Configure FlowProvider

Wrap your application with FlowProvider to enable all React SDK features.

) } export default Root`} language="tsx" />
Mainnet
https://rest-mainnet.onflow.org
Testnet
https://access-testnet.onflow.org
3

Start building

Import and use components and hooks from the React SDK

{user &&

Welcome, {user.addr}!

}
) }`} language="tsx" />

Important Notes

  • • FlowProvider must wrap your entire application at the root level
  • • For Next.js, place FlowProvider in layout.tsx with "use client" directive
  • • React 18+ and TypeScript are recommended for best experience
) } ================================================ FILE: packages/demo/src/components/starter-banner.tsx ================================================ export function StarterBanner() { return (

Looking for a starter?

Try our Next.js starter template with Flow React SDK pre-configured and ready to be used.

View Starter

Building for mobile?

Try our Expo starter template with Flow React Native SDK pre-configured and ready to be used.

View Starter
) } ================================================ FILE: packages/demo/src/components/ui/code-editor.tsx ================================================ import {useState, useRef, useEffect} from "react" import {Prism as SyntaxHighlighter} from "react-syntax-highlighter" import {oneDark, oneLight} from "react-syntax-highlighter/dist/esm/styles/prism" import {useDarkMode} from "../flow-provider-wrapper" interface CodeEditorProps { value: string onChange: (value: string) => void language?: string placeholder?: string className?: string minHeight?: string } export function CodeEditor({ value, onChange, language = "javascript", // Use javascript as fallback for Cadence since it's similar placeholder = "Enter your code here...", className = "", minHeight = "150px", }: CodeEditorProps) { const {darkMode} = useDarkMode() const textareaRef = useRef(null) const [isFocused, setIsFocused] = useState(false) // Auto-resize textarea useEffect(() => { const textarea = textareaRef.current if (textarea) { textarea.style.height = "auto" textarea.style.height = Math.max(150, textarea.scrollHeight) + "px" } }, [value]) return (
{/* Syntax highlighted background */}
{value || " "}
{/* Editable textarea overlay */}