diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0e6732f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = tab + +[*.{dart,yml,yaml}] +indent_style = space +indent_size = 2 diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..c925921 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,21 @@ +name: Bug report +description: Report a reproducible problem +title: "[Bug]: " +body: + - type: textarea + id: behavior + attributes: + label: Expected and actual behavior + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Minimal reproduction + description: Include package/runtime versions, platform, and steps. Remove keys, credentials, cookies and personal data. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Sanitized error codes and logs diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..fdca6d1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Private vulnerability report + url: https://github.com/bchainhub/flutter_connect/security/advisories/new + about: Report security vulnerabilities privately. diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..4729c0d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,14 @@ +name: Feature request +description: Suggest an improvement +title: "[Feature]: " +body: + - type: textarea + id: problem + attributes: + label: Problem and proposed behavior + validations: + required: true + - type: textarea + id: compatibility + attributes: + label: Compatibility and security considerations diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..92674ce --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ +## Change + +Describe the problem and resulting behavior. + +## Validation + +List relevant tests and compatibility effects. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..598a504 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI +on: + push: + branches: [main, master] + pull_request: + workflow_call: +permissions: + contents: read +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + - run: flutter pub get + - run: dart format --output=none --set-exit-if-changed lib test example/lib example/test + - run: flutter analyze + - run: flutter test + - run: flutter test + working-directory: example + - run: flutter pub publish --dry-run + android: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + - uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + - run: flutter pub get + - run: flutter build apk --debug + working-directory: example + ios: + runs-on: macos-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + - run: flutter pub get + - run: flutter build ios --simulator --debug + working-directory: example diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3659bac --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,32 @@ +name: Release +on: + push: + tags: ['[0-9]*.[0-9]*.[0-9]*'] +permissions: + contents: read +jobs: + validate: + uses: ./.github/workflows/ci.yml + tag: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Verify version + run: | + version=$(sed -n 's/^version: //p' pubspec.yaml) + test "$GITHUB_REF_NAME" = "$version" + publish: + needs: [validate, tag] + permissions: + id-token: write + uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 + release: + needs: publish + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - run: gh release create "$GITHUB_REF_NAME" --verify-tag --generate-notes + env: + GH_TOKEN: ${{ github.token }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8136d67 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.dart_tool/ +.flutter-plugins-dependencies +build/ +.DS_Store +# Dependency lockfiles +*.lock +package-lock.json +npm-shrinkwrap.json +pnpm-lock.yaml +bun.lockb +Package.resolved + +# Generated protocol artifacts +/vectors/ +/vendor/ +/.connect-sdk/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e3e3a0a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +## 0.1.0 + +Initial Connect v1 implementation, shared conformance vectors, security tests, examples and release automation. + +- Add named chain presets, wallet adapters and extensible server verifiers for all fourteen requested chains, plus Bitcoin legacy and XRP Ed25519. Expand shared fixtures and live cross-language authentication tests. + +- Use connect.js for browser-only authentication and wallet adapters. better-connect independently owns HTTP endpoints, backend state transitions and Better Auth sessions. + +- Rename the browser wallet entry to connect.js/wallet; add brand-neutral discovery, Solana and CIP-30 adapters, TronLink-style signing and native wallet bridges. Add shared Core Blockchain Ed448/ICAN helpers and a future-wallet bridge with mainnet/Devin support. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d6417b9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +CORE License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), +to to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +All distribution of the Covered Software in Source Code Form, including any +Modifications and/or Contributions must be disclosed and publicly available. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE TO ANY CLAIM, DAMAGES OR +OTHER LIABILITIES, WHETHER IN AN ACTION OF A CONTRACT, TORT, OR OTHERWISE, +ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 1009e97..be56d8c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,78 @@ # flutter_connect -Connect Protocol + +Flutter wallet library for Connect v1. Handles full `connect://` links and QR text for any dapp; validates the challenge, asks the user to approve signing, and returns an encrypted proof through Bluetooth or another channel. Better Auth is optional. See [nearby integration](doc/NEARBY.md). + +```yaml +dependencies: + flutter_connect: ^0.1.0 +``` + +```dart +final connect = ConnectClient( + accountProvider: myWalletAccountProvider, +); +connect.requests.listen((request) { + // Present domain/origin, account, network, profile and expiry in trusted UI. +}); +final request = await connect.resolve(scannedQrText); +// From an explicit Approve button only: +await request.approve(request.compatibleAccounts.first); +``` + +A `WalletAccountProvider` returns `WalletAccount` objects with a `SigningSelection` and `sign(Uint8List canonicalPayload)`. The selection contains `WalletIdentity(namespace, reference, address)`, profile and COSE algorithm (or null). Signatures are returned as `WalletSignature`; private keys remain inside the account implementation. `ConnectRequest` exposes immutable `challenge`, `compatibleAccounts`, `state`, `approve`, `reject` and `cancel`. + +```dart +class XcbAccount implements WalletAccount { + @override + final SigningSelection selection; + final Future Function(Uint8List) nativeSign; + XcbAccount(String address, this.nativeSign) + : selection = SigningSelection( + account: WalletIdentity(namespace: 'core', reference: '1', address: address), + profile: 'xcb-ed448', alg: coseEd448); + @override + Future sign(Uint8List payload) => nativeSign(payload); +} +``` + +The native XCB signer must use pure Ed448 over the supplied bytes, without extra hashing/prefixing, and return a 57-byte public key plus 114-byte signature as lowercase hex. An Ethereum adapter selects `eip155`, decimal chain reference, an EIP-55 address, `ethereum-siwe`, and null algorithm; it passes the canonical bytes to EIP-191 personal_sign. Bitcoin selects `bitcoin-bip322-p2wpkh` with its BIP122 reference and signs using BIP-322 simple. The host implements wallet signing; the SDK encrypts responses without receiving private signing keys. + +```dart +WidgetsFlutterBinding.ensureInitialized(); +final links = ConnectLinks(client: connect, onError: showSafeError); +await links.start(); // cold-start and running-app links +// On owner disposal: await links.dispose(); await connect.dispose(); +``` + +Register Android/iOS schemes using [platform setup](doc/PLATFORMS.md); the checked-in [example](example) includes the required manifest/plist entries and an ephemeral Ed25519 account with a native confirmation screen. QR scanners, clipboard controls and navigation remain host-app choices. For direct invocation use `resolveTarget(ConnectTarget(origin, requestId))`. HTTPS/Universal/App Links resolve to identical canonical bytes. Same-device login uses the same protocol and manual return to the browser; arbitrary callback URLs are rejected. + +A full handoff needs no `ConnectNetwork`. Short-URI HTTP compatibility requires explicitly passing `network: HttpsNetwork()`. `responseReady` means the proof is ready to return, not that portal sign-in succeeded. + +```dart +// After showing and confirming request.challenge.origin: +final peripheral = ConnectBluetoothPeripheral( + handoff: request.handoff!, onError: showSafeError, +); +await peripheral.start(); +// From an explicit signing approval: +await request.approve(request.compatibleAccounts.first); +peripheral.publish(request.response!); +// On cancellation/disposal: await peripheral.stop(); +``` + + +Named presets cover Core Blockchain, Ethereum, Polygon, Base, Bitcoin, Solana, BNB Smart Chain, TRON, Monero, Stellar, Litecoin, XRP, Zcash and Cardano. Use `ChainWalletAccount(chain: ConnectChains.solana, address: address, sign: callback)` to avoid configuring low-level profiles. See [supported addresses and wallet methods](doc/CHAINS.md). Full details are in the [protocol](doc/PROTOCOL.md) and [research notes](doc/IMPLEMENTATION.md). + +```sh +flutter pub get +dart format --output=none --set-exit-if-changed lib test example/lib example/test +flutter analyze +flutter test +cd example && flutter test +# From the sibling better-connect checkout: +node scripts/flutter-interop.mjs +``` + +Shared vectors assert identical Dart/TypeScript messages. Dart Ed25519 signatures and an OpenSSL Ed448 host bridge also complete real Better Auth desktop sessions. The OpenSSL fixture requires OpenSSL 3 on the test host; it is not a mobile signing implementation. The standalone suite skips live-server tests unless the integration runner supplies its test endpoint. + +See [release setup](doc/RELEASE.md). Licensed under the [CORE License](LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..88f309f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security policy + +Report vulnerabilities privately through this repository's GitHub Security Advisories. Do not post private keys, redemption credentials, session cookies or exploitable proof material in public issues. Version 0.1.x is the initial supported line. + +See doc/PROTOCOL.md for the threat model, supported profiles and transport restrictions. This is a new protocol implementation with automated security tests; it has not received an independent security audit. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..801d885 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,4 @@ +analyzer: + exclude: + - build/** +include: package:flutter_lints/flutter.yaml diff --git a/doc/CHAINS.md b/doc/CHAINS.md new file mode 100644 index 0000000..e440025 --- /dev/null +++ b/doc/CHAINS.md @@ -0,0 +1,91 @@ +# Chains and wallet adapters + +Connect authenticates a wallet key/address. It does not transfer coins, enumerate tokens, check balances, or grant token permissions. Tokens on supported chains use the same wallet identity; no per-token signing profile is necessary. + +Select named chains in ordinary application code. Profiles remain explicit on the wire so a verifier cannot confuse signing formats. `allChains` / `ConnectChains.all` includes the fourteen requested mainnets and two alternate signing methods (Bitcoin legacy and XRP Ed25519). Raw Ed25519 remains available explicitly for development or applications using raw-key identities. + +| Preset (TypeScript / Dart) | Identity and network | Wallet signing operation | Proof signature / publicKey | Supported addresses | +| --- | --- | --- | --- | --- | +| `core` | `core:1` | Pure Ed448 on canonical bytes | lowercase hex / 57-byte key hex | Core Blockchain XCB ICAN `cb…`; this is not Core DAO | +| `ethereum` | `eip155:1` | EIP-191 `personal_sign` of SIWE | `0x` hex / absent | EIP-55 EOA | +| `polygon` | `eip155:137` | Same SIWE method | `0x` hex / absent | Polygon PoS EOA | +| `base` | `eip155:8453` | Same SIWE method | `0x` hex / absent | Base EOA | +| `bnb` | `eip155:56` | Same SIWE method | `0x` hex / absent | BNB Smart Chain EOA | +| `bitcoin` | `bip122:000000000019d6689c085ae165831e93` | BIP-322 simple | canonical padded base64 / absent | Native SegWit v0 P2WPKH `bc1q…`, SIGHASH_ALL | +| `bitcoinLegacy` | Same Bitcoin network | Bitcoin `signmessage` | canonical padded base64 / absent | Legacy P2PKH `1…`, compressed or uncompressed key | +| `solana` | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | Ed25519 `signMessage` on canonical bytes | 64-byte lowercase hex / absent | Base58 Ed25519 public key, no PDA | +| `tron` | `tron:728126428` | `signMessageV2` | `0x` lowercase hex / absent | Base58Check `T…` key address | +| `monero` | `monero:418015bb9ae982a1975da7d79277c270` | `sign` with `signature_type: spend` | Original `SigV2…` string / absent | Mainnet standard and subaddresses; no integrated address or view-key proof | +| `stellar` | `stellar:pubnet` | SEP-53 message signing | 64-byte lowercase hex / absent | `G…` Ed25519 StrKey, no muxed/contract address | +| `litecoin` | `bip122:12a765e31ffd4059bada1e25190f6e98` | Litecoin `signmessage` | canonical padded base64 / absent | Legacy P2PKH `L…` | +| `ripple` | `xrpl:0` | `ripple-keypairs.sign(messageHex, privateKey)` | lowercase DER hex / compressed key hex | XRP classic `r…`, secp256k1 | +| `rippleEd25519` | Same XRP network | Same API with Ed25519 key | 64-byte lowercase hex / `ed`-prefixed 33-byte key hex | XRP classic `r…`, Ed25519 | +| `zcash` | `bip122:00040fe8ec8471911baa1db1266ea15d` | Zcash `signmessage` | canonical padded base64 / absent | Transparent P2PKH `t1…`; no shielded/unified addresses | +| `cardano` | `cip34:1-764824073` | CIP-30 `signData(addressHex, payloadHex)` | COSE_Sign1 lowercase hex / COSE_Key lowercase hex | Mainnet Shelley base with key payment credential, enterprise key address, or reward key address | + +Signing method availability depends on the host wallet. These are protocol adapters and verifiers, not integrations with every named wallet application. XRP wallets exposing only a transaction or proprietary sign-in API need a compatible message-signing bridge. Transactions are never substituted for authentication messages. + +## TypeScript wallet + +```ts +import { chains, walletAccount } from 'connect.js'; + +const account = walletAccount(chains.solana, address, async bytes => { + const result = await wallet.signMessage(bytes); + return { signature: Array.from(result.signature, b => b.toString(16).padStart(2, '0')).join('') }; +}); +// Return this account from WalletAccountProvider.getAccounts(). +``` + +A provider can expose several entries for the same address if it supports different methods, such as `chains.bitcoin` versus `chains.bitcoinLegacy`. `accountFor(chain, address)` supplies an immutable selection without a signer. `defineChain` defines additional metadata, and `evmChain('Arbitrum', 42161)` adds an EVM network using the existing SIWE verifier. The relying application must explicitly enable the same network in its challenge requirements. + +For Ethereum-compatible providers, pass the UTF-8 canonical bytes as `0x` hex to `personal_sign`, with the selected EIP-55 address. For Cardano, pass those bytes as payload hex to CIP-30 and return `{ signature: result.signature, publicKey: result.key }`; `addressHex` must correspond to the selected Bech32 address. For Stellar, a SEP-53 wallet API applies SHA-256 to `Stellar Signed Message:\n` plus canonical bytes; a raw Ed25519 signer must implement that wrapper. TRON, UTXO and Monero wallet APIs apply their own prescribed wrappers. Never prefix or hash twice. Core and Solana sign the raw canonical bytes. + +## Flutter wallet + +```dart +final account = ChainWalletAccount( + chain: ConnectChains.solana, + address: address, + sign: (bytes) async { + final signature = await wallet.signMessage(bytes); + return WalletSignature(signature.map((b) => b.toRadixString(16).padLeft(2, '0')).join()); + }, +); +``` + +`ConnectChain.evm('Arbitrum', 42161)` mirrors TypeScript. The `ConnectChain` constructor supports other protocol definitions. Keys stay in the host wallet. Wallet entry points do not load the signature verifiers or Monero WASM. + +## Better Auth + +```ts +import { betterConnect, allChains, chains } from 'better-connect'; + +betterConnect({ origin: 'https://example.com', chains: allChains }); +// Or explicitly choose a subset: +betterConnect({ origin: 'https://example.com', chains: [chains.core, chains.ethereum, chains.solana] }); +``` + +The lower-level `requirements` option remains supported. Supply either `chains` or `requirements`, never both; empty policy is rejected. Authentication is opt-in, not automatically expanded when more chains become available in a future release. + +To add a new signing standard, implement `SigningProfile`, or use `defineSigningProfile({ id, namespace, alg, validateAccount, verify })`. The wrapper rejects malformed library results and only returns the claimed address after successful validation. Extend with `new ProfileRegistry().with(customProfile)` and a matching `defineChain` definition. A new EVM network needs only `evmChain`. A Bitcoin-like P2PKH network can use `utxoMessageProfile` with its exact genesis reference, signed-message magic, and address-version bytes. Add positive, wrong-key, network, mutation and replay vectors before enabling any new scheme. Defining metadata alone never installs a verifier. + +## Verification boundaries + +All profiles prove possession of the key associated with the selected address. They do not inspect live account permissions, master-key disabling, weighted/multisignature policies, delegated signers, or contract account validation. In particular, Stellar/XRP/TRON ledger authority may differ from key possession. ERC-1271 and script wallets are not implemented. Only the address types in the table are accepted; no signature-format guessing or automatic fallback occurs. + +Cardano enforces protected EdDSA (`-8` inside legacy CIP-8 COSE, Ed25519 curve 6 and OKP key type 1), the exact protected address, the payload and a matching Blake2b-224 key credential. The Connect algorithm remains the fully specified Ed25519 identifier `-19`. Attached unhashed and Blake2b-224-hashed payloads are supported. Protected headers must be exactly algorithm and address; unprotected headers exactly `hashed`; COSE_Key exactly kty, alg, crv and x. Unknown/critical extensions, duplicate map keys, detached payloads, pointers, scripts and Byron addresses are rejected. + +Monero verification lazily loads monero-project WASM through monero-ts, operates offline, validates the address checksum/network, and accepts only modern spend-key SigV2 proofs. Each verification closes its temporary in-memory wallet; it does not persist a wallet or contact a daemon. This is heavier than the other verifiers and requires WebAssembly. connect.js ships browser WASM chunks; better-connect owns the Node WASM loader and bundles its patched Monero dependency. The Bitcoin legacy signing library is used only in better-connect tests as an independent signing implementation; production recovery uses noble. Remaining low-severity elliptic audit notices are limited to that development dependency. + +## Standards and references + +- [SIWE / ERC-4361](https://eips.ethereum.org/EIPS/eip-4361), [BIP-322](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki). +- [Solana keys and PDAs](https://solana.com/docs/core/pda), [TRON signMessageV2](https://tronweb.network/docu/docs/API%20List/trx/signMessageV2/). +- [Stellar SEP-53](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md), [XRP keypairs](https://github.com/XRPLF/xrpl.js/tree/main/packages/ripple-keypairs). +- [Monero wallet RPC](https://www.getmonero.org/resources/developer-guides/wallet-rpc.html), [monero-ts](https://github.com/woodser/monero-ts). +- [Cardano CIP-8](https://cips.cardano.org/cip/CIP-8), [CIP-30](https://cips.cardano.org/cip/CIP-30), [CIP-34](https://cips.cardano.org/cip/CIP-34). +- [Litecoin signed messages](https://github.com/litecoin-project/litecoin/blob/master/src/util/message.cpp), [Zcash message magic](https://github.com/zcash/zcash/blob/master/src/main.cpp), [Zcash address versions and genesis](https://github.com/zcash/zcash/blob/master/src/chainparams.cpp). +- [CAIP namespaces](https://namespaces.chainagnostic.org/), [Base chain ID](https://docs.base.org/base-chain/api-reference/ethereum-json-rpc-api/eth_chainId), [Polygon chain ID](https://docs.polygon.technology/wallets/wallet-operations), [BNB configuration](https://docs.bnbchain.org/bnb-smart-chain/developers/wallet-configuration/). + +The main `connect.js` import provides browser-only challenge creation and verification. `connect.js/verification` exposes signature profiles, and `connect.js/wallet` exposes wallet discovery and adapters. better-connect owns its backend engine, HTTP endpoints and session storage. connect.js has no server or dapp entry point. diff --git a/doc/IMPLEMENTATION.md b/doc/IMPLEMENTATION.md new file mode 100644 index 0000000..d683e63 --- /dev/null +++ b/doc/IMPLEMENTATION.md @@ -0,0 +1,30 @@ +# Implementation plan + +1. Define Connect v1, strict schemas, canonical messages and shared vectors in connect.js. +2. Build profile verification and atomic request transitions, then Better Auth endpoints and normal session cookies. +3. Port wallet behavior to Dart, including lifecycle, transport and explicit approval. +4. Exercise cryptographic substitution attacks, concurrency, cross-language bytes and real Better Auth sessions. +5. Add release automation, issue templates, examples and platform documentation. + +Research: CAIP-122 provides field vocabulary; Ethereum uses a valid EIP-4361 message with Connect bindings in Resources. Other profiles use a fixed ASCII line format, never JSON serialization. Ed448 = -53; Ed25519 = -19 (IANA/RFC9864). XCB address derivation is SHA3-256(publicKey)[12:] plus network prefix and ICAN checksum in go-core. No XCB registration was found in the CAIP namespace registry: use explicitly local namespace core, not an asserted registered CAIP identifier. Generic Ed25519 uses an explicitly raw-key identity. Bitcoin support will be limited to BIP322 simple P2WPKH. Wallet signing remains delegated to host implementations. + +Sources: +- https://better-auth.com/docs/plugins/siwe +- https://better-auth.com/docs/concepts/plugins +- https://standards.chainagnostic.org/CAIPs/caip-2 +- https://standards.chainagnostic.org/CAIPs/caip-10 +- https://standards.chainagnostic.org/CAIPs/caip-122 +- https://www.iana.org/assignments/cose/cose.xhtml +- https://www.rfc-editor.org/rfc/rfc9864.html +- https://raw.githubusercontent.com/core-coin/go-core/master/crypto/crypto.go +- https://raw.githubusercontent.com/core-coin/go-core/master/common/types.go +- https://cip.coreblockchain.net/cip/core/cip-98/ +- https://eips.ethereum.org/EIPS/eip-191 +- https://eips.ethereum.org/EIPS/eip-4361 +- https://eips.ethereum.org/EIPS/eip-1271 +- https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki +- https://docs.flutter.dev/ui/navigation/deep-linking +- https://developer.android.com/training/app-links/create-deeplinks +- https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app +- https://pub.dev/packages/cryptography +- https://pub.dev/packages/app_links diff --git a/doc/NEARBY.md b/doc/NEARBY.md new file mode 100644 index 0000000..f3acfe2 --- /dev/null +++ b/doc/NEARBY.md @@ -0,0 +1,29 @@ +# Nearby and offline Connect + +Connect supports any dapp. Better Auth is an optional portal integration. + +1. The portal creates a challenge and pairing ticket, then displays the full `connect://` URI as a QR code or link. +2. The wallet validates the embedded challenge, displays the website, chain, account and expiry, and asks the user to approve. No HTTP lookup is needed. +3. The wallet signs with its account signer and encrypts the proof for this ticket. +4. The portal receives the packet over Bluetooth or another channel, decrypts it, and verifies the signature and its binding to the claimed wallet address against its original challenge. +5. A browser-only dapp uses the verified identity locally. A Better Auth portal submits the proof to its server, which verifies it and creates a session after separate redemption. + +## Bluetooth and other channels + +The native wallet advertises as a BLE peripheral; the browser connects as the central. After scanning, confirm the website and enable Bluetooth in the wallet, keep it in the foreground, and click the portal's device chooser. Approval releases the encrypted response. Stop on cancellation, expiry or disposal. Run one peripheral handoff at a time. + +Web Bluetooth requires a secure context, a user gesture and permission, and has limited browser availability. Feature-detect `navigator.bluetooth`; a browser tab cannot advertise. See [MDN Web Bluetooth](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API). Flutter uses [bluetooth_low_energy](https://github.com/yanshouwang/bluetooth_low_energy); native peripheral support covers Android, iOS, macOS and Windows, subject to hardware and permissions. Linux and Flutter Web use another response channel. Physical-device validation is required before deployment. + +No internet or database is needed for browser verification or the wallet exchange once application assets are available. Loading an uncached site needs connectivity. A Better Auth session requires a reachable server, possibly on a local network. Bluetooth cannot create that server session while the server is unreachable. + +The packet is a string: clipboard/manual transfer, a response QR scanner, a host bridge or a local-network channel can deliver it to the same `accept` method. Hosts supply scanners or bridges; WebRTC/NFC/Wi-Fi Direct adapters are not included. Large multi-chain challenges may exceed one QR's capacity: request relevant chains or use the full link/text. + +## Pairing and trust + +The full URI is `connect:///connect/v1/#`. Payload is unpadded base64url of UTF-8 JSON with exactly `type: "connect:handoff:1"`, `challenge` and `key`. The key is 32 fresh random bytes encoded as 64 lowercase hex characters. Never log or persist the QR/link. Better Auth's redemption secret is separate and never enters the payload. + +The response has exactly `type: "connect:response:1"`, `requestId`, `iv` and `ciphertext`. AES-256-GCM uses a fresh 12-byte nonce; IV and ciphertext plus the 16-byte tag are unpadded base64url. Additional authenticated data is UTF-8 `/`. Limits are 32,768 characters for a URI or packet and 16,384 bytes for plaintext proof. Verification enforces expiry and single-use acceptance. + +Encryption protects the return channel; signature verification still proves the wallet address. An offline QR does not prove ownership of its claimed website: display and explicitly confirm the origin, or pin it through a trusted channel. A copied QR carries its pairing key. Do not infer physical proximity or phishing resistance from BLE alone. + +The service UUID uses the first 32 hex characters of requestId in UUID formatting. Control characteristic `b6c01402-df5b-4b79-9f8d-8a642b9cd001` accepts a four-byte little-endian offset. Response characteristic `b6c01403-df5b-4b79-9f8d-8a642b9cd001` returns little-endian total length and offset (four bytes each), then at most 12 bytes. A zero-length header at offset zero means approval is pending. Frames fit a 20-byte ATT payload. Lengths and offsets are bounded and checked. diff --git a/doc/PLATFORMS.md b/doc/PLATFORMS.md new file mode 100644 index 0000000..7d225ad --- /dev/null +++ b/doc/PLATFORMS.md @@ -0,0 +1,54 @@ +# Flutter platform integration + +Create `ConnectLinks` early after `WidgetsFlutterBinding.ensureInitialized()`, subscribe to `client.requests`, and call `links.start()`. The helper receives both cold-start and running-app links using app_links raw-string APIs. It suppresses duplicate delivery of the same string; the client additionally rejects already-handled requests. Call `links.dispose()` and `client.dispose()` when the owner is destroyed. QR scanners and clipboard UI pass their decoded text to `client.resolve(text)`; no scanner or clipboard dependency is imposed. + +## Android + +Inside the wallet activity in AndroidManifest.xml: + +```xml + + + + + + + +``` + +Add the INTERNET permission at the manifest root. Keep the activity exported for link dispatch and use singleTop launch mode. The checked-in example includes this configuration. Test cold and warm dispatch with `adb shell am start -W -a android.intent.action.VIEW -d 'connect://example.com/connect/v1/<64-hex-id>'`. + +For verified App Links, add a separate autoVerify HTTPS intent filter scoped to your domain and `/connect/v1/`, and serve `/.well-known/assetlinks.json` with the real application ID and SHA-256 release-signing certificate fingerprint. Keep custom-scheme and verified HTTPS filters separate. Do not use placeholder association files in production. + +## iOS + +In Runner/Info.plist: + +```xml +FlutterDeepLinkingEnabled +CFBundleURLTypes + + CFBundleURLNameyour.wallet.connect + CFBundleURLSchemesconnect + +``` + +Test with `xcrun simctl openurl booted 'connect://example.com/connect/v1/<64-hex-id>'`. Universal Links also require an Associated Domains entitlement `applinks:your.domain` and a served `/.well-known/apple-app-site-association` file associating your Team ID and Bundle ID with `/connect/v1/*`. Actual domain ownership, signing identities and installed-device association must be verified by the host application. + +## Multiple wallets and other platforms + +Custom schemes do not establish app ownership. Android may show a resolver, apply a saved default or follow OS routing policy; iOS does not guarantee a chooser or deterministic routing between apps registering the same scheme. `connect://` is not a secure universal wallet chooser. The protocol and OS dispatch are separate concerns. Prefer domain-verified links for app ownership, or an explicitly selected wallet-specific scheme via `UriTransport(schemes: {'mywallet'})`. + +Core parsing, canonicalization and wallet state are portable to Flutter desktop/web. app_links platform registration and application lifecycle differ across macOS, Windows and Linux; the example targets Android/iOS only. Browser CORS, forbidden redirects and secure origin access also apply to Flutter web. Hardware-backed keys need host-native bridges; the package does not export private signing keys. Dart cryptography supplies the example's Ed25519; XCB Ed448 is delegated to a native wallet signer. The OpenSSL bridge in tests is a desktop conformance fixture, not a mobile key-management implementation. + +## Troubleshooting + +`invalidUri`: use exact lowercase syntax, no query, ports, IP addresses or whitespace. `domainMismatch`: align the QR, TLS endpoint, challenge origin and server baseURL. `noCompatibleAccount`: configure a requirement matching the wallet's namespace, network, profile and algorithm. `expiredRequest`: create a new request and check device clocks. `replayedRequest`: reuse the existing request UI rather than reopening its URI. `serverRejected`: inspect safe server error codes and verify the wallet is signing canonical bytes without an extra prefix/hash. Unverified HTTPS links may open the browser; registration and association files belong to the host app, not the protocol package. + +Sources: [Flutter deep links](https://docs.flutter.dev/ui/navigation/deep-linking), [Android link dispatch](https://developer.android.com/training/app-links/create-deeplinks), [Apple custom schemes](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app). + +## Nearby Bluetooth + +Android requires minSdk 24. Add `BLUETOOTH_ADVERTISE` and `BLUETOOTH_CONNECT` to the manifest, plus legacy `BLUETOOTH` and `BLUETOOTH_ADMIN` with `android:maxSdkVersion="30"`. The peripheral requests runtime permission when starting. iOS and macOS require `NSBluetoothAlwaysUsageDescription`; macOS also needs its Bluetooth entitlement if sandboxed. The example includes Android/iOS configuration. Keep the app foregrounded for advertising; background operation is not implemented. + +Check `ConnectBluetoothPeripheral.isSupportedPlatform` before constructing it. Show and confirm the requested origin before calling `start()`, publish `request.response` only after approval, and call `stop()` on cancellation or disposal. A full handoff URI intentionally carries a fragment; the legacy short lookup URI does not. See [wire framing and fallback channels](NEARBY.md). diff --git a/doc/PROTOCOL.md b/doc/PROTOCOL.md new file mode 100644 index 0000000..1c172d7 --- /dev/null +++ b/doc/PROTOCOL.md @@ -0,0 +1,81 @@ +# Connect protocol v1 + +`connect://` is the primary transport URI. The Connect authentication protocol itself is transport-independent. HTTPS, Universal Links, App Links, wallet-specific schemes and direct invocation carry the same public request identity. + +## Wire format + +The primary app handoff embeds the challenge and pairing key in a URI fragment and needs no server lookup. See [nearby and offline transport](NEARBY.md). Canonical signature bytes remain unchanged. The short-URI HTTP lookup below is an optional Better Auth compatibility transport. + +`connect://example.com/connect/v1/` maps to `https://example.com/api/auth/wallet-connect/challenge?requestId=`. + +The HTTPS alternative is `https://example.com/connect/v1/`. Domains must be lowercase ASCII DNS names, with at least two labels. Punycode is permitted and displayed literally. Ports, IP literals, credentials, fragments on short lookup URIs, queries, percent escapes, trailing dots, path normalization and whitespace are rejected. Local servers are supported only through an explicitly injected test transport/network, never by disabling TLS validation. The Better Auth base URL must be exactly `/api/auth`. + +IDs and nonces are 32 random bytes encoded as 64 lowercase hex characters. Redemption secrets use the same entropy but are separate credentials. A challenge is a strict object containing `version: 1`, `requestId`, `nonce`, `domain`, `origin`, `issuedAt`, `expiresAt`, and `requirements`. Times are UTC `YYYY-MM-DDTHH:mm:ss.sssZ`, with a maximum lifetime of five minutes and 30 seconds of allowed issuance clock skew. Each requirement contains `namespace`, `reference`, `profile`, and `alg`. `alg` is an integer or explicit JSON null. Unknown fields are rejected. + +A proof contains `requestId`, `account: { namespace, reference, address }`, `profile`, `alg`, `signature`, and optional `publicKey`. The verifier loads its stored challenge and never accepts a client-supplied nonce, message, domain or expiration. EdDSA keys/signatures are lowercase hex without `0x`; Ethereum signatures are `0x`-prefixed lowercase hex; BIP-322 simple signatures are canonical padded Base64. + +The canonical message is UTF-8, LF-separated, with no trailing LF, BOM, JSON serialization or Unicode normalization. For non-Ethereum profiles: + +```text +Connect Authentication +Version: 1 +Domain: +URI: +Account: ::
+Profile: +Algorithm: +Nonce: +Request ID: +Issued At: +Expiration Time: +``` + +Ethereum uses an ERC-4361 message constructed by `canonicalMessage`, with the same fields and Connect version/profile/algorithm/account bindings in Resources. Its fixed statement is `Approve this Connect sign-in request.`. See `test/fixtures/conformance.json` for the exact bytes of every profile. Requirements select acceptable identities before signing; the selected identity/profile/algorithm are signed, while the server independently enforces its stored policy. + +## Profiles + +The original profiles below remain stable. [Chain profiles and adapters](CHAINS.md) specify the expanded set, named presets, and extension APIs. + +| Profile | Namespace / reference | Algorithm | Identity verification | +| --- | --- | --- | --- | +| `xcb-ed448` | local `core` / `1` (mainnet), `3` (Devin) | Ed448, COSE -53 | Ed448 signature plus SHA3-256(publicKey) last 20 bytes, cb/ab network prefix and ICAN checksum | +| `raw-ed25519` | local `raw` / `ed25519` | Ed25519, COSE -19 | Address is the exact 32-byte public key in lowercase hex; strict RFC8032 verification | +| `ethereum-siwe` | CAIP `eip155` / decimal chain ID | null | ERC-4361 message signed with EIP-191; recovered EOA equals the EIP-55 address | +| `bitcoin-bip322-p2wpkh` | CAIP `bip122` / `000000000019d6689c085ae165831e93` | null | Mainnet BIP-322 simple native P2WPKH, compressed key and SIGHASH_ALL | + +The local `core` and `raw` namespaces are explicitly protocol-local identifiers, not claims of CAIP registration. The raw-key profile does not claim to implement Solana or another Ed25519 chain. XCB uses pure RFC8032 Ed448 over these exact canonical bytes (empty context); do not pass the bytes to a Core wallet API that adds a personal-message prefix or hashes them again. This is an explicit Connect signing method, not an alias for Core transaction signing. Address derivation is checked against a go-core reference vector. Host wallets using CIP-98 derived keys need a compatible native signer; private keys never enter the SDK API. + +Ethereum personal_sign is not generic COSE ES256K. Contract accounts/ERC-1271 are currently rejected by the EOA profile. The BIP-322 profile rejects P2SH, P2TR and arbitrary scripts. A separate `bitcoin-signmessage` profile supports legacy P2PKH. See [chain profiles](CHAINS.md) for all additional networks, exact wrappers, proof encodings and address limits. Future profiles must define full canonical signing and key/account verification semantics; they cannot weaken an existing profile. + +## Endpoints and lifecycle + +All paths below are relative to `/api/auth/wallet-connect`. + +| Method/path | Input | Result | +| --- | --- | --- | +| POST `/create` | `{}`; exact relying-party Origin header | requestId, connectUri, redeemSecret, expiresAt | +| GET `/challenge` | requestId query | pending challenge | +| GET `/status` | requestId query | requestId and status only | +| POST `/approve` | proof | requestId and APPROVED; no session/cookie | +| POST `/redeem` | requestId and redeemSecret; exact Origin header | success and user ID; Better Auth session cookie | +| POST `/deny` | requestId and redeemSecret; exact Origin header | requestId and DENIED | + +Server: PENDING -> APPROVED -> CONSUMED; PENDING -> DENIED; PENDING/APPROVED -> EXPIRED. Conditional database updates choose exactly one winner. Browser polling is sufficient; the status API can later be backed by SSE/WebSocket notifications without altering proofs. + +Wallet: awaitingUserApproval -> signing -> submitting -> approved, or rejected/cancelled/expired/failed. Resolving/fetching is represented by the pending resolve Future/Promise. Opening a URI never signs. Confirmation shows validated origin/domain, identity, network, profile and expiration. Expiration is checked immediately before signing and submission. Cancellation while a hardware signer is running suppresses submission after it returns; cancellation cannot undo a proof already submitted. In-memory duplicate tracking lasts for the client instance. A local wallet rejection does not remotely deny a public QR: only the initiating browser can call deny. Local rejection/cancellation does not revoke an already-approved request. + +## Security boundary + +A wallet address is a claim. Authentication is established only when the server verifies that the signing key corresponds to that claimed account. + +Client-supplied wallet addresses and public keys are untrusted. A user is authenticated only after the server cryptographically verifies the signature and establishes that the signing key corresponds to the claimed blockchain account. + +A QR contains only a public request identifier. It never contains a redemption secret, signature, private key or session token. The phone authorizes a fresh desktop session; it never transfers its own session. The desktop must retain its secret privately until redemption. Do not place it in URLs, analytics, browser history or logs. Clear it after completion. Public challenge/status responses do not disclose the approved identity. + +TLS origin matching prevents silently signing for a different relying party. It does not prove that a displayed QR came from the intended desktop; an attacker can relay a QR for the same legitimate site. The wallet must make cross-device authorization explicit, and users must scan their own initiating browser. No arbitrary callbacks/return URLs are accepted in v1. Same-device login follows the identical flow, with the browser retaining its secret and the user returning manually. + +Wallet HTTP requests omit cookies (TypeScript), reject redirects, time out after 15 seconds, and cap response bytes at 16 KiB. Dart's default native http client has no cookie jar; injected clients must preserve that property. No private keys or raw authentication material are logged by the packages. Error codes are stable strings without raw server/signer errors. Configure server body limits and Better Auth rate limiting at deployment. Browser wallet hosts should explicitly configure trusted origins/CORS where necessary; native clients do not require browser CORS. + +## Conformance + +`test/fixtures/conformance.json` is identical in all repositories. It contains public deterministic test seeds for EdDSA, proofs, complete canonical strings and hex bytes. Never use these seeds for assets or production authentication. The server tests verify every built-in profile, wrong-key injection, mutation, replay, expiry and concurrent redemption. `scripts/flutter-interop.mjs` in better-connect starts an isolated real Better Auth/SQLite server and runs all seventeen chain/method combinations through the full QR/approval/redemption flow, using Dart Ed25519, native OpenSSL Ed448 and test host-wallet bridges for the other methods. diff --git a/doc/RELEASE.md b/doc/RELEASE.md new file mode 100644 index 0000000..4a2d308 --- /dev/null +++ b/doc/RELEASE.md @@ -0,0 +1,16 @@ +# Release setup + +CI runs formatting/linting, static checks, relevant tests and package validation. Flutter additionally builds Android and iOS simulator examples. better-connect's release also runs cross-repository Flutter interoperability. No release has been pushed or published as part of implementation. + +1. Review the package version and changelog. Keep v1 protocol vectors identical across all three repositories. +2. Configure repository Actions and the registry's trusted publisher for `bchainhub/flutter_connect` and `release.yml`. For npm use its GitHub OIDC trusted publishing settings; for pub.dev enable automated GitHub publishing with the `{{version}}` tag pattern. Initial package creation/ownership and registry settings must be established by maintainers. +3. Ensure dependent repositories are pushed before cross-repository CI runs. For incompatible future protocol changes pin the integration checkout to a matching release rather than silently mixing versions. +4. Push `` to trigger validation, publication and a GitHub release. The tag must match the manifest exactly. + +CORE License text is copied unchanged from the reference flutter_txms repository. npm metadata uses `SEE LICENSE IN LICENSE` because CORE is a custom license. + +For better-connect, CI checks out the matching SDK source and runs `node scripts/prepare-sdk.mjs` before installing dependencies. The helper builds the ignored vendor tarball; `node scripts/sync-protocol.mjs` refreshes a local installation. Reference conformance fixtures live in `test/fixtures/` and remain versioned. Generated `vectors/` and `vendor/` directories are ignored. The published bundle includes the SDK so registry users do not need local filesystem dependencies. + +References: [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/), [Dart publishing workflow](https://github.com/dart-lang/setup-dart/blob/main/.github/workflows/publish.yml). + +Dependency lockfiles are ignored. npm workflows install from package.json with `--package-lock=false`; npm cache keys use package.json. Flutter workflows already resolve dependencies with `flutter pub get`. diff --git a/doc/VALIDATION.md b/doc/VALIDATION.md new file mode 100644 index 0000000..a70098b --- /dev/null +++ b/doc/VALIDATION.md @@ -0,0 +1,50 @@ +# Implementation validation — 2026-09-07 + +Implemented locally in flutter_connect, connect.js and better-connect. No commits, pushes, tags or registry publications were made in the target repositories. + +## Results + +| Check | Result | +| --- | --- | +| connect.js browser API/unit/conformance/lifecycle suite | 39 passed | +| Chromium browser and installed-package verification | All 17 proof fixtures, fresh Core flow, lifecycle checks and static example passed | +| better-connect cryptographic/security/SQLite/client suite | 54 passed | +| Flutter standalone suite | 27 passed; the 17 live-server cases run separately below | +| Live Flutter interoperability runner | 18 passed: repeated OpenSSL vector check plus 17 complete sign-in flows | +| Flutter example widget test | 1 passed | +| TypeScript checks, including inferred Better Auth client API | Passed | +| ESLint and Prettier checks | Passed | +| Dart formatting and Flutter analysis | Passed | +| Android debug APK and iOS simulator builds | Passed | +| Android emulator cold/warm custom-scheme dispatch | Passed; both delivered to Flutter URI validation | +| iPhone 17 Pro simulator cold/warm custom-scheme dispatch | Passed; cold invalid URI reached validation, warm canonical URI reached HTTPS retrieval and reported serverRejected for the deliberately nonexistent example.com endpoint | +| Identical vectors across three repositories | SHA-256 f244e3eefe4b8c64f85b74204a077f2d0bcdb89dde9579410ca7fecf24671d17 | +| SDK and plugin packaged installation | Passed, including all 17 proof fixtures, browser-only SDK packaging and the plugin's patched Node Monero dependency | +| pub.dev dry run | Passed with zero warnings from a clean temporary source snapshot | +| GitHub workflow YAML parsing | Passed | +| npm dependency audit during clean installation | Zero production advisories in installed package; development-only bitcoinjs-message has three low-severity transitive notices | + +The Node suites now contain 39 SDK and 54 Better Auth tests; the removed standalone HTTP-server tests have been replaced by browser API and installed-package coverage. The OpenSSL vector case runs again in the integration runner. The normal Flutter command intentionally skips the live-server cases when no endpoint is supplied; all seventeen passed under better-connect/scripts/flutter-interop.mjs. Native device link tests and cryptographic server integration were exercised separately: the latter runs Dart on the host with an injected loopback test transport, not a deployed mobile HTTPS service. + +Local toolchain: Node 24.2.0, Flutter 3.47.2 / Dart 3.13.2, OpenSSL 3.6.4, Better Auth 1.7.3. CI also defines a Node 22 job; that version's hosted CI execution has not been run locally. The direct pub dry run in the working tree warns about the intentionally uncommitted README; the clean snapshot check verifies release packaging without committing user changes. + +## Delivered architecture and support + +connect.js owns the browser-only API, strict v1 schemas, canonicalization, wallet adapters and local signature verification. It has no server, HTTP dapp client or request store. better-connect owns the backend request engine, Node Monero loading, persistent adapter storage, HTTP endpoints and Better Auth session cookies. Flutter ports the wallet protocol to Dart and adds app_links lifecycle integration. Shared vectors verify canonical bytes across all three packages. + +Named presets and built-in verification cover Core Blockchain, Ethereum, Polygon, Base, Bitcoin, Solana, BNB Smart Chain, TRON, Monero, Stellar, Litecoin, XRP, Zcash and Cardano, plus alternate Bitcoin legacy and XRP Ed25519 methods. Raw Ed25519 remains supported. [CHAINS.md](CHAINS.md) specifies address limits and encoding rules. Tests cover every chain through real signature verification, unrelated-key substitution, message-field changes, network/algorithm mismatches and one-time redemption. Additional checks use the official Core and Stellar vectors, independent Emurgo CIP-8 signing, Cardano protected-header and credential tampering, and rejection of Monero view-key authentication. + +The live runner uses native OpenSSL for Core, Dart Ed25519 for the raw-key fixture, and test-only host-wallet bridges for the other chain methods. All seventeen paths traverse the Dart wallet lifecycle and actual Better Auth session creation. They do not claim integration with every mobile wallet or hardware device. Earlier native Android/iOS build and link checks remain applicable; this expansion changes protocol adapters and verification, not platform registration. + +The URI is connect:///connect/v1/<64-hex-request-id>; see PROTOCOL.md for exact schemas, signing semantics, endpoint routes, security boundaries and errors, and PLATFORMS.md for Android/iOS registration. + +## Deployment prerequisites and limits + +- Configure registry ownership/trusted publishing before using release tags. Cross-repository CI requires access to the sibling repository; private repositories need an appropriately scoped checkout credential. +- Host wallets provide signing adapters, including mobile Ed448/native key management. The OpenSSL fixture is only a test-host bridge. +- The local core/raw namespace labels are not claimed as registered CAIP namespaces. Confirm a future official XCB namespace before changing the versioned protocol mapping. +- ERC-1271, script/multisignature account authority, automatic callbacks, and address types outside the explicit chain table are unsupported. +- The default database path is tested against real SQLite. Other Better Auth adapters must preserve conditional updates and unique constraints; run their database integration tests before deployment. Session-creation failure after consumption requires a new sign-in request. +- Universal/App Link domain associations require the host's real domain, signing identities and deployed association files. These domain-verification flows and physical hardware signers were not exercised. + +Browser coverage includes local request creation, EIP-1193 account and chain checks before/after signing, origin binding, expiry, cancellation and single-use verification. The installed tarball is exercised in Chromium with networking and persistent storage disabled. All seventeen verifier fixtures run through the browser API and through Better Auth's independently owned backend. Package checks assert that server files, request-store APIs and the server/dapp/browser entry points are absent. diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..79f7eca --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,48 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# Widget Preview related +.widget_preview/ diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 0000000..986c516 --- /dev/null +++ b/example/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d3b14c876900e553bc736ca19295fc09e3853e8e" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d3b14c876900e553bc736ca19295fc09e3853e8e + base_revision: d3b14c876900e553bc736ca19295fc09e3853e8e + - platform: android + create_revision: d3b14c876900e553bc736ca19295fc09e3853e8e + base_revision: d3b14c876900e553bc736ca19295fc09e3853e8e + - platform: ios + create_revision: d3b14c876900e553bc736ca19295fc09e3853e8e + base_revision: d3b14c876900e553bc736ca19295fc09e3853e8e + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..bb93ab1 --- /dev/null +++ b/example/README.md @@ -0,0 +1,9 @@ +# Connect reference wallet + +Run `flutter pub get` and `flutter run` on Android/iOS. The example registers `connect://`, receives cold/warm links, and accepts QR-decoded or pasted URI text through its input field. It displays validated relying-party and account information and signs only after the Approve button is pressed. + +The example uses a freshly generated in-memory Ed25519 account. Configure better-connect with `{ namespace: 'raw', reference: 'ed25519', profile: 'raw-ed25519', alg: -19 }`. The key is lost when the application restarts; this is a test wallet, not a persistent account manager. + +For Core and Ethereum wallets, replace DemoAccounts with your native account provider as described in the package README. XCB signs pure canonical Ed448 bytes; Ethereum uses personal_sign over the canonical SIWE bytes. Do not put private keys in application configuration or source files. + +Platform registration and HTTPS association instructions are in [PLATFORMS.md](../doc/PLATFORMS.md). `flutter test` runs the initial-screen widget test. The sibling better-connect integration runner exercises actual Dart signatures and desktop session redemption. diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 0000000..0da40e9 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,34 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + - build/** + - android/** + - ios/** + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/example/android/.gitignore b/example/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/example/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/example/android/app/build.gradle.kts b/example/android/app/build.gradle.kts new file mode 100644 index 0000000..acc2669 --- /dev/null +++ b/example/android/app/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + id("com.android.application") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "net.bchainhub.connect_wallet_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "net.bchainhub.connect_wallet_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = 24 + targetSdk = flutter.targetSdkVersion + // Uses the version code from pubspec.yaml. When using split APKs, 1000 * ABI_VERSION + // is added automatically by Flutter. (https://developer.android.com/studio/build/configure-apk-splits#configure-APK-versions) + // You can force using the value of versionCode by specifying the `-P force-version-code-ignoring-abi=true` + // flag during build. + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +flutter { + source = "../.." +} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b47fc0d --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/kotlin/net/bchainhub/connect_wallet_example/MainActivity.kt b/example/android/app/src/main/kotlin/net/bchainhub/connect_wallet_example/MainActivity.kt new file mode 100644 index 0000000..7a59b71 --- /dev/null +++ b/example/android/app/src/main/kotlin/net/bchainhub/connect_wallet_example/MainActivity.kt @@ -0,0 +1,5 @@ +package net.bchainhub.connect_wallet_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/build.gradle.kts b/example/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/example/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 0000000..e96108c --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +# This newDsl flag was added by the Flutter template +android.newDsl=false +# This builtInKotlin flag was added by the Flutter template +android.builtInKotlin=false diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a20f2c4 --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip diff --git a/example/android/settings.gradle.kts b/example/android/settings.gradle.kts new file mode 100644 index 0000000..b28021a --- /dev/null +++ b/example/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "9.1.0" apply false + id("org.jetbrains.kotlin.android") version "2.4.0" apply false +} + +include(":app") diff --git a/example/ios/.gitignore b/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..391a902 --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/example/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..dcf0759 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,647 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.bchainhub.connectWalletExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = net.bchainhub.connectWalletExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = net.bchainhub.connectWalletExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = net.bchainhub.connectWalletExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.bchainhub.connectWalletExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.bchainhub.connectWalletExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c3fedb2 --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..c30b367 --- /dev/null +++ b/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist new file mode 100644 index 0000000..b94631c --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,78 @@ + + + + + NSBluetoothAlwaysUsageDescription + Return approved Connect wallet proofs to a nearby browser. + FlutterDeepLinkingEnabled + CFBundleURLTypes + + CFBundleURLNamenet.bchainhub.connect + CFBundleURLSchemesconnect + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Connect Wallet Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + connect_wallet_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/example/ios/Runner/SceneDelegate.swift b/example/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/example/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..b59d08a --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,228 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:cryptography/cryptography.dart'; +import 'package:flutter/material.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:flutter_connect/flutter_connect.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + runApp(const MaterialApp(home: ConnectDemo())); +} + +String hex(List bytes) => + bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + +class DemoAccount implements WalletAccount { + final SimpleKeyPair _key; + @override + final SigningSelection selection; + DemoAccount(this._key, String address) + : selection = SigningSelection( + account: WalletIdentity( + namespace: 'raw', + reference: 'ed25519', + address: address, + ), + profile: 'raw-ed25519', + alg: coseEd25519, + ); + @override + Future sign(Uint8List canonicalPayload) async { + final signature = await Ed25519().sign(canonicalPayload, keyPair: _key); + return WalletSignature( + hex(signature.bytes), + publicKey: selection.account.address, + ); + } +} + +class DemoAccounts implements WalletAccountProvider { + Future? _account; + Future _create() async { + final key = await Ed25519().newKeyPair(); + return DemoAccount(key, hex((await key.extractPublicKey()).bytes)); + } + + @override + Future> getAccounts() async => [ + await (_account ??= _create()), + ]; +} + +class ConnectDemo extends StatefulWidget { + const ConnectDemo({super.key}); + @override + State createState() => _ConnectDemoState(); +} + +class _ConnectDemoState extends State { + final input = TextEditingController(); + ConnectBluetoothPeripheral? bluetooth; + late final ConnectClient client; + late final ConnectLinks links; + StreamSubscription? subscription; + ConnectRequest? request; + String? error; + bool loading = false; + @override + void initState() { + super.initState(); + client = ConnectClient(accountProvider: DemoAccounts()); + subscription = client.requests.listen((r) { + if (request?.challenge.requestId != r.challenge.requestId) { + unawaited(bluetooth?.stop()); + bluetooth = null; + } + if (mounted) { + setState(() { + request = r; + error = null; + }); + } + }); + links = ConnectLinks(client: client, onError: showError); + unawaited(links.start().catchError(showError)); + } + + void showError(Object e) { + if (mounted) { + setState( + () => error = e is ConnectException + ? e.code + : 'Unable to process request', + ); + } + } + + Future open() async { + setState(() { + loading = true; + request = null; + error = null; + }); + try { + await bluetooth?.stop(); + bluetooth = null; + await client.handleUri(input.text); + } catch (e) { + showError(e); + } + if (mounted) setState(() => loading = false); + } + + @override + void dispose() { + unawaited(links.dispose()); + unawaited(subscription?.cancel()); + unawaited(client.dispose()); + unawaited(bluetooth?.stop()); + input.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final r = request; + return Scaffold( + appBar: AppBar(title: const Text('Connect wallet')), + body: ListView( + padding: const EdgeInsets.all(24), + children: [ + const Text( + 'Reference wallet', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + const Text( + 'Uses an ephemeral Ed25519 test account. Use the nearby dapp example, which allows raw-ed25519. A real wallet supplies its own account and signing implementation.', + ), + const SizedBox(height: 24), + TextField( + controller: input, + decoration: const InputDecoration( + labelText: 'Paste a Connect URI', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + FilledButton( + onPressed: loading ? null : open, + child: Text(loading ? 'Loading…' : 'Review request'), + ), + if (error != null) + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + 'Request error: $error', + style: const TextStyle(color: Colors.red), + ), + ), + if (r != null) ...[ + if (r.handoff != null && + r.state == ConnectState.awaitingUserApproval) + OutlinedButton( + onPressed: () async { + try { + await bluetooth?.stop(); + final peripheral = ConnectBluetoothPeripheral( + handoff: r.handoff!, + onError: showError, + ); + bluetooth = peripheral; + await peripheral.start(); + } catch (e) { + showError(e); + } + }, + child: const Text( + 'Confirm website and enable nearby Bluetooth', + ), + ), + if (r.response != null) ...[ + const Text( + 'Return this encrypted response to the portal. If Bluetooth is enabled, the portal can collect it now.', + ), + QrImageView(data: r.response!, size: 300), + SelectableText(r.response!), + ], + const Divider(height: 40), + const Text( + 'Sign in on another device?', + style: TextStyle(fontSize: 22), + ), + const SizedBox(height: 12), + Text( + r.challenge.domain, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text(r.challenge.origin), + Text('Expires: ${r.challenge.expiresAt}'), + SelectableText( + 'Account: ${r.compatibleAccounts.first.selection.account.address}', + ), + Text('Profile: ${r.compatibleAccounts.first.selection.profile}'), + Text('Status: ${r.state.name}'), + if (r.state == ConnectState.awaitingUserApproval) ...[ + FilledButton( + onPressed: () async { + try { + await r.approve(r.compatibleAccounts.first); + if (r.response != null) { + bluetooth?.publish(r.response!); + } + } catch (e) { + showError(e); + } + }, + child: const Text('Approve sign-in'), + ), + OutlinedButton(onPressed: r.reject, child: const Text('Reject')), + ], + ], + ], + ), + ); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..fda9c3b --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,19 @@ +name: connect_wallet_example +description: Connect wallet reference application using an ephemeral Ed25519 test account. +publish_to: none +version: 0.1.0+1 +environment: + sdk: '>=3.9.0 <4.0.0' +dependencies: + flutter: + sdk: flutter + flutter_connect: + path: ../ + cryptography: ^2.7.0 + qr_flutter: ^4.1.0 +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 +flutter: + uses-material-design: true diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart new file mode 100644 index 0000000..28c7996 --- /dev/null +++ b/example/test/widget_test.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:connect_wallet_example/main.dart'; + +void main() { + testWidgets('wallet starts with review action and no automatic approval', ( + tester, + ) async { + await tester.pumpWidget(const MaterialApp(home: ConnectDemo())); + await tester.pump(); + expect(find.text('Review request'), findsOneWidget); + expect(find.text('Approve sign-in'), findsNothing); + }); +} diff --git a/lib/flutter_connect.dart b/lib/flutter_connect.dart new file mode 100644 index 0000000..0536e4d --- /dev/null +++ b/lib/flutter_connect.dart @@ -0,0 +1,9 @@ +library; + +export 'src/protocol.dart'; +export 'src/wallet.dart'; +export 'src/links.dart'; +export 'src/chains.dart'; + +export 'src/handoff.dart'; +export 'src/bluetooth.dart'; diff --git a/lib/src/bluetooth.dart b/lib/src/bluetooth.dart new file mode 100644 index 0000000..a3b51ef --- /dev/null +++ b/lib/src/bluetooth.dart @@ -0,0 +1,189 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:bluetooth_low_energy/bluetooth_low_energy.dart'; +import 'handoff.dart'; +import 'protocol.dart'; + +/// Native wallet peripheral. The portal is the Web Bluetooth central. +/// Call start only after displaying/confirming the scanned request's origin. +class ConnectBluetoothPeripheral { + final ConnectHandoff handoff; + final PeripheralManager _manager; + final void Function(Object) onError; + final ConnectBleResponseBuffer _buffer = ConnectBleResponseBuffer(); + final Map _offsets = {}; + final List> _subscriptions = []; + GATTService? _service; + Timer? _timer; + bool _active = false; + bool _starting = false; + ConnectBluetoothPeripheral({ + required this.handoff, + required this.onError, + PeripheralManager? manager, + }) : _manager = manager ?? PeripheralManager(); + static bool get isSupportedPlatform => + !kIsWeb && + [ + TargetPlatform.android, + TargetPlatform.iOS, + TargetPlatform.macOS, + TargetPlatform.windows, + ].contains(defaultTargetPlatform); + Future start() async { + if (_active || _starting) fail('invalidState'); + if (!isSupportedPlatform) fail('bluetoothUnavailable'); + handoff.challenge.validateTime(DateTime.now()); + _active = true; + _starting = true; + try { + if (defaultTargetPlatform == TargetPlatform.android && + !await _manager.authorize()) { + fail('bluetoothPermissionDenied'); + } + if (_manager.state == BluetoothLowEnergyState.unknown) { + await _manager.stateChanged + .firstWhere( + (event) => event.state != BluetoothLowEnergyState.unknown, + ) + .timeout(const Duration(seconds: 15)); + } + if (_manager.state != BluetoothLowEnergyState.poweredOn) { + fail('bluetoothUnavailable'); + } + if (!_active) fail('cancelled'); + final control = GATTCharacteristic.mutable( + uuid: UUID.fromString(connectBleControlUuid), + properties: [GATTCharacteristicProperty.write], + permissions: [GATTCharacteristicPermission.write], + descriptors: [], + ); + final response = GATTCharacteristic.mutable( + uuid: UUID.fromString(connectBleResponseUuid), + properties: [GATTCharacteristicProperty.read], + permissions: [GATTCharacteristicPermission.read], + descriptors: [], + ); + final service = GATTService( + uuid: UUID.fromString(handoff.serviceUuid), + isPrimary: true, + includedServices: [], + characteristics: [control, response], + ); + _subscriptions.add( + _manager.characteristicWriteRequested.listen((event) { + if (event.characteristic.uuid != control.uuid) return; + unawaited(_write(event).catchError(onError)); + }, onError: onError), + ); + _subscriptions.add( + _manager.characteristicReadRequested.listen((event) { + if (event.characteristic.uuid != response.uuid) return; + unawaited(_read(event).catchError(onError)); + }, onError: onError), + ); + await _manager.addService(service); + _service = service; + if (!_active) { + fail('cancelled'); + } + handoff.challenge.validateTime(DateTime.now()); + await _manager.startAdvertising( + Advertisement(serviceUUIDs: [service.uuid]), + ); + if (!_active) { + await _manager.stopAdvertising(); + fail('cancelled'); + } + _timer = Timer( + DateTime.parse(handoff.challenge.expiresAt).difference(DateTime.now()), + () => unawaited(stop().catchError(onError)), + ); + } catch (_) { + await stop(); + rethrow; + } finally { + _starting = false; + } + } + + /// Publish only the encrypted response returned by ConnectRequest.response. + void publish(String response) { + if (!_active) fail('invalidState'); + handoff.challenge.validateTime(DateTime.now()); + final data = object(jsonDecode(response), [ + 'type', + 'requestId', + 'iv', + 'ciphertext', + ]); + if (data['type'] != 'connect:response:1' || + data['requestId'] != handoff.challenge.requestId) { + fail('invalidHandoff'); + } + _buffer.publish(response); + } + + Future _write(GATTCharacteristicWriteRequestedEventArgs event) async { + try { + handoff.challenge.validateTime(DateTime.now()); + final bytes = event.request.value; + if (!_active || + event.request.offset != 0 || + bytes.length != 4 || + (!_offsets.containsKey(event.central.uuid) && + _offsets.length >= 16)) { + fail('invalidBluetoothFrame'); + } + final offset = ByteData.sublistView(bytes).getUint32(0, Endian.little); + if (offset > 32768) fail('invalidBluetoothFrame'); + _offsets[event.central.uuid] = offset; + } catch (_) { + await _manager.respondWriteRequestWithError( + event.request, + error: GATTError.invalidPDU, + ); + return; + } + await _manager.respondWriteRequest(event.request); + } + + Future _read(GATTCharacteristicReadRequestedEventArgs event) async { + Uint8List value; + try { + handoff.challenge.validateTime(DateTime.now()); + if (!_active || event.request.offset != 0) fail('invalidBluetoothFrame'); + value = _buffer.frame(_offsets[event.central.uuid] ?? 0); + } catch (_) { + await _manager.respondReadRequestWithError( + event.request, + error: GATTError.invalidOffset, + ); + return; + } + await _manager.respondReadRequestWithValue(event.request, value: value); + } + + Future stop() async { + if (!_active && _service == null) return; + _active = false; + _timer?.cancel(); + _timer = null; + for (final subscription in _subscriptions) { + await subscription.cancel(); + } + _subscriptions.clear(); + _offsets.clear(); + _buffer.clear(); + final service = _service; + _service = null; + if (service != null) { + try { + await _manager.stopAdvertising(); + } finally { + await _manager.removeService(service); + } + } + } +} diff --git a/lib/src/chains.dart b/lib/src/chains.dart new file mode 100644 index 0000000..91f0ab6 --- /dev/null +++ b/lib/src/chains.dart @@ -0,0 +1,243 @@ +import 'dart:typed_data'; +import 'wallet.dart'; +import 'protocol.dart'; + +/// Canonical UTF-8 bytes must be passed unchanged to [signingMethod]. +/// Encodings describe the returned proof fields. +class ConnectChain { + final String name, + namespace, + reference, + profile, + signingMethod, + signatureEncoding; + final String? publicKeyEncoding; + final int? alg; + const ConnectChain({ + required this.name, + required this.namespace, + required this.reference, + required this.profile, + required this.alg, + required this.signingMethod, + required this.signatureEncoding, + this.publicKeyEncoding, + }); + factory ConnectChain.evm(String name, int chainId) { + if (chainId <= 0 || chainId > 999999999999999) fail('invalidChain'); + return ConnectChain( + name: name, + namespace: 'eip155', + reference: '$chainId', + profile: 'ethereum-siwe', + alg: null, + signingMethod: 'personal_sign', + signatureEncoding: 'hex0x', + ); + } + SigningRequirement get requirement => + SigningRequirement(namespace, reference, profile, alg); + SigningSelection account(String address) => SigningSelection( + account: WalletIdentity( + namespace: namespace, + reference: reference, + address: address, + ), + profile: profile, + alg: alg, + ); +} + +abstract final class ConnectChains { + static const core = ConnectChain( + name: 'Core Blockchain', + namespace: 'core', + reference: '1', + profile: 'xcb-ed448', + alg: -53, + signingMethod: 'ed448', + signatureEncoding: 'hex', + publicKeyEncoding: 'hex', + ); + static const ethereum = ConnectChain( + name: 'Ethereum', + namespace: 'eip155', + reference: '1', + profile: 'ethereum-siwe', + alg: null, + signingMethod: 'personal_sign', + signatureEncoding: 'hex0x', + ); + static const polygon = ConnectChain( + name: 'Polygon', + namespace: 'eip155', + reference: '137', + profile: 'ethereum-siwe', + alg: null, + signingMethod: 'personal_sign', + signatureEncoding: 'hex0x', + ); + static const base = ConnectChain( + name: 'Base', + namespace: 'eip155', + reference: '8453', + profile: 'ethereum-siwe', + alg: null, + signingMethod: 'personal_sign', + signatureEncoding: 'hex0x', + ); + static const bnb = ConnectChain( + name: 'BNB Smart Chain', + namespace: 'eip155', + reference: '56', + profile: 'ethereum-siwe', + alg: null, + signingMethod: 'personal_sign', + signatureEncoding: 'hex0x', + ); + static const bitcoin = ConnectChain( + name: 'Bitcoin', + namespace: 'bip122', + reference: '000000000019d6689c085ae165831e93', + profile: 'bitcoin-bip322-p2wpkh', + alg: null, + signingMethod: 'bip322-simple', + signatureEncoding: 'base64', + ); + static const solana = ConnectChain( + name: 'Solana', + namespace: 'solana', + reference: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + profile: 'solana-ed25519', + alg: -19, + signingMethod: 'signMessage', + signatureEncoding: 'hex', + ); + static const tron = ConnectChain( + name: 'TRON', + namespace: 'tron', + reference: '728126428', + profile: 'tron-signmessage-v2', + alg: null, + signingMethod: 'signMessageV2', + signatureEncoding: 'hex0x', + ); + static const monero = ConnectChain( + name: 'Monero', + namespace: 'monero', + reference: '418015bb9ae982a1975da7d79277c270', + profile: 'monero-spend-v2', + alg: null, + signingMethod: 'sign-spend', + signatureEncoding: 'sigv2', + ); + static const stellar = ConnectChain( + name: 'Stellar', + namespace: 'stellar', + reference: 'pubnet', + profile: 'stellar-sep53', + alg: -19, + signingMethod: 'sep53', + signatureEncoding: 'hex', + ); + static const litecoin = ConnectChain( + name: 'Litecoin', + namespace: 'bip122', + reference: '12a765e31ffd4059bada1e25190f6e98', + profile: 'litecoin-signmessage', + alg: null, + signingMethod: 'signmessage', + signatureEncoding: 'base64', + ); + static const ripple = ConnectChain( + name: 'XRP Ledger', + namespace: 'xrpl', + reference: '0', + profile: 'xrpl-secp256k1', + alg: null, + signingMethod: 'sign', + signatureEncoding: 'hex', + publicKeyEncoding: 'hex', + ); + static const zcash = ConnectChain( + name: 'Zcash', + namespace: 'bip122', + reference: '00040fe8ec8471911baa1db1266ea15d', + profile: 'zcash-signmessage', + alg: null, + signingMethod: 'signmessage', + signatureEncoding: 'base64', + ); + static const cardano = ConnectChain( + name: 'Cardano', + namespace: 'cip34', + reference: '1-764824073', + profile: 'cardano-cip8', + alg: -19, + signingMethod: 'signData', + signatureEncoding: 'cose-hex', + publicKeyEncoding: 'cose-hex', + ); + static const bitcoinLegacy = ConnectChain( + name: 'Bitcoin (legacy P2PKH)', + namespace: 'bip122', + reference: '000000000019d6689c085ae165831e93', + profile: 'bitcoin-signmessage', + alg: null, + signingMethod: 'signmessage', + signatureEncoding: 'base64', + ); + static const rippleEd25519 = ConnectChain( + name: 'XRP Ledger (Ed25519)', + namespace: 'xrpl', + reference: '0', + profile: 'xrpl-ed25519', + alg: -19, + signingMethod: 'sign', + signatureEncoding: 'hex', + publicKeyEncoding: 'hex', + ); + static const all = [ + core, + ethereum, + polygon, + base, + bnb, + bitcoin, + solana, + tron, + monero, + stellar, + litecoin, + ripple, + zcash, + cardano, + bitcoinLegacy, + rippleEd25519, + ]; +} + +List requirementsFor(Iterable chains) { + final result = {}; + for (final chain in chains) { + final r = chain.requirement; + result['${r.namespace}:${r.reference}:${r.profile}:${r.alg}'] = r; + } + return List.unmodifiable(result.values); +} + +/// Connect adapter around an existing wallet's message-signing operation. +class ChainWalletAccount implements WalletAccount { + @override + final SigningSelection selection; + final Future Function(Uint8List) _sign; + ChainWalletAccount({ + required ConnectChain chain, + required String address, + required Future Function(Uint8List) sign, + }) : selection = chain.account(address), + _sign = sign; + @override + Future sign(Uint8List canonicalPayload) => + _sign(canonicalPayload); +} diff --git a/lib/src/handoff.dart b/lib/src/handoff.dart new file mode 100644 index 0000000..a77c76b --- /dev/null +++ b/lib/src/handoff.dart @@ -0,0 +1,117 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:cryptography/cryptography.dart'; +import 'protocol.dart'; + +const connectBleControlUuid = 'b6c01402-df5b-4b79-9f8d-8a642b9cd001'; +const connectBleResponseUuid = 'b6c01403-df5b-4b79-9f8d-8a642b9cd001'; +String handoffServiceUuid(String id) { + identifier(id); + return '${id.substring(0, 8)}-${id.substring(8, 12)}-${id.substring(12, 16)}-${id.substring(16, 20)}-${id.substring(20, 32)}'; +} + +String _encode(List bytes) => base64Url.encode(bytes).replaceAll('=', ''); +List _decode(String text) { + if (!matches(r'[A-Za-z0-9_-]+', text)) fail('invalidHandoff'); + final bytes = base64Url.decode(base64Url.normalize(text)); + if (_encode(bytes) != text) fail('invalidHandoff'); + return bytes; +} + +/// The full challenge and a secret response-encryption key. Never log the URI. +class ConnectHandoff { + final ConnectChallenge challenge; + final SecretKey _key; + final DateTime Function() _now; + ConnectHandoff._(this.challenge, this._key, this._now); + + /// expectedOrigin must be trusted or explicitly confirmed by the wallet user. + factory ConnectHandoff.parse( + String uri, { + String? expectedOrigin, + DateTime Function()? now, + }) { + try { + if (uri.length > 32768) fail('invalidHandoff'); + final parts = uri.split('#'); + if (parts.length != 2 || parts[1].isEmpty) fail('invalidHandoff'); + final target = UriTransport(schemes: {'connect'}).parse(parts[0]); + final data = object(jsonDecode(utf8.decode(_decode(parts[1]))), [ + 'type', + 'challenge', + 'key', + ]); + if (data['type'] != 'connect:handoff:1' || + data['key'] is! String || + !matches(r'[a-f0-9]{64}', data['key']) || + (expectedOrigin != null && + target.origin != validateOrigin(expectedOrigin))) { + fail('invalidHandoff'); + } + final clock = now ?? DateTime.now; + final challenge = ConnectChallenge.fromJson( + data['challenge'], + target: target, + now: clock(), + ); + final key = data['key'] as String; + return ConnectHandoff._( + challenge, + SecretKey([ + for (var i = 0; i < key.length; i += 2) + int.parse(key.substring(i, i + 2), radix: 16), + ]), + clock, + ); + } catch (_) { + fail('invalidHandoff'); + } + } + String get serviceUuid => handoffServiceUuid(challenge.requestId); + Future sealProof(Map proof) async { + challenge.validateTime(_now()); + if (proof['requestId'] != challenge.requestId) fail('invalidHandoff'); + final bytes = utf8.encode(jsonEncode(proof)); + if (bytes.length > 16384) fail('invalidHandoff'); + final box = await AesGcm.with256bits().encrypt( + bytes, + secretKey: _key, + aad: utf8.encode('${challenge.origin}/${challenge.requestId}'), + ); + challenge.validateTime(_now()); + return jsonEncode({ + 'type': 'connect:response:1', + 'requestId': challenge.requestId, + 'iv': _encode(box.nonce), + 'ciphertext': _encode([...box.cipherText, ...box.mac.bytes]), + }); + } +} + +/// Bounded transport framing shared with Web Bluetooth. No key or plaintext proof is exposed. +class ConnectBleResponseBuffer { + Uint8List? _bytes; + void publish(String response) { + final bytes = Uint8List.fromList(utf8.encode(response)); + if (bytes.isEmpty || bytes.length > 32768) fail('invalidHandoff'); + _bytes = bytes; + } + + void clear() { + _bytes = null; + } + + Uint8List frame(int offset) { + final bytes = _bytes; + if (offset < 0 || offset > (bytes?.length ?? 0)) { + fail('invalidBluetoothFrame'); + } + final length = bytes == null ? 0 : (bytes.length - offset).clamp(0, 12); + final frame = Uint8List(8 + length); + final header = ByteData.sublistView(frame); + header.setUint32(0, bytes?.length ?? 0, Endian.little); + header.setUint32(4, offset, Endian.little); + if (length > 0) frame.setRange(8, frame.length, bytes!, offset); + return frame; + } +} diff --git a/lib/src/links.dart b/lib/src/links.dart new file mode 100644 index 0000000..b7ad75b --- /dev/null +++ b/lib/src/links.dart @@ -0,0 +1,39 @@ +import 'dart:async'; +import 'package:app_links/app_links.dart'; +import 'wallet.dart'; + +/// Create early after WidgetsFlutterBinding.ensureInitialized(). +/// Host applications own navigation and display errors through [onError]. +class ConnectLinks { + final ConnectClient client; + final void Function(Object error) onError; + final AppLinks _links; + StreamSubscription? _subscription; + final Set _delivered = {}; + ConnectLinks({required this.client, required this.onError, AppLinks? links}) + : _links = links ?? AppLinks(); + Future _handle(String uri) async { + if (!_delivered.add(uri)) return; + try { + await client.handleUri(uri); + } catch (e) { + onError(e); + } + } + + Future start() async { + if (_subscription != null) return; + // String APIs preserve raw syntax; Uri.toString could normalize hostile input. + _subscription = _links.stringLinkStream.listen( + (s) => unawaited(_handle(s)), + onError: onError, + ); + final initial = await _links.getInitialLinkString(); + if (initial != null) await _handle(initial); + } + + Future dispose() async { + await _subscription?.cancel(); + _subscription = null; + } +} diff --git a/lib/src/protocol.dart b/lib/src/protocol.dart new file mode 100644 index 0000000..6743821 --- /dev/null +++ b/lib/src/protocol.dart @@ -0,0 +1,270 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +const coseEd448 = -53; +const coseEd25519 = -19; + +class ConnectException implements Exception { + final String code; + const ConnectException(this.code); + @override + String toString() => 'ConnectException($code)'; +} + +Never fail(String code) => throw ConnectException(code); +bool matches(String pattern, String value) => + RegExp('^(?:$pattern)\$').firstMatch(value)?.end == value.length; +String atom(Object? value) { + if (value is! String || + value.isEmpty || + value.length > 128 || + !matches(r'[a-zA-Z0-9._-]+', value)) { + fail('malformedResponse'); + } + return value; +} + +String identifier(Object? value) { + if (value is! String || !matches(r'[a-f0-9]{64}', value)) { + fail('invalidRequest'); + } + return value; +} + +Map object(Object? value, List keys) { + if (value is! Map || + value.length != keys.length || + !keys.every(value.containsKey)) { + fail('malformedResponse'); + } + return value; +} + +bool validDomain(String domain) => + domain.length <= 253 && + domain.contains('.') && + !matches(r'\d+(\.\d+){3}', domain) && + domain + .split('.') + .every((l) => matches(r'[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?', l)); +String validateOrigin(String origin) { + if (!origin.startsWith('https://') || !validDomain(origin.substring(8))) { + fail('invalidUri'); + } + return origin; +} + +class ConnectTarget { + final String origin; + final String requestId; + ConnectTarget(String origin, String requestId) + : origin = validateOrigin(origin), + requestId = identifier(requestId); + String get connectUri => + 'connect://${origin.substring(8)}/connect/v1/$requestId'; +} + +abstract interface class ConnectTransport { + ConnectTarget parse(String input); +} + +class UriTransport implements ConnectTransport { + final Set schemes; + UriTransport({Set schemes = const {'connect', 'https'}}) + : schemes = Set.unmodifiable(schemes); + @override + ConnectTarget parse(String input) { + if (input.length > 512) fail('invalidUri'); + final m = RegExp( + r'^([a-z][a-z0-9+.-]*):\/\/([^/]+)\/connect\/v1\/([a-f0-9]{64})$', + ).firstMatch(input); + if (m == null || + m.end != input.length || + !schemes.contains(m[1]) || + !validDomain(m[2]!)) { + fail('invalidUri'); + } + return ConnectTarget('https://${m[2]}', m[3]!); + } +} + +class WalletIdentity { + final String namespace, reference, address; + WalletIdentity({ + required String namespace, + required String reference, + required String address, + }) : namespace = atom(namespace), + reference = atom(reference), + address = atom(address); + factory WalletIdentity.fromJson(Object? value) { + final j = object(value, ['namespace', 'reference', 'address']); + return WalletIdentity( + namespace: atom(j['namespace']), + reference: atom(j['reference']), + address: atom(j['address']), + ); + } + Map toJson() => { + 'namespace': namespace, + 'reference': reference, + 'address': address, + }; +} + +class SigningSelection { + final WalletIdentity account; + final String profile; + final int? alg; + SigningSelection({ + required this.account, + required String profile, + required this.alg, + }) : profile = atom(profile); +} + +class SigningRequirement { + final String namespace, reference, profile; + final int? alg; + SigningRequirement(this.namespace, this.reference, this.profile, this.alg); + factory SigningRequirement.fromJson(Object? value) { + final j = object(value, ['namespace', 'reference', 'profile', 'alg']); + if (j['alg'] != null && j['alg'] is! int) fail('invalidChallenge'); + return SigningRequirement( + atom(j['namespace']), + atom(j['reference']), + atom(j['profile']), + j['alg'] as int?, + ); + } + bool accepts(SigningSelection s) => + namespace == s.account.namespace && + reference == s.account.reference && + profile == s.profile && + alg == s.alg; + Map toJson() => { + 'namespace': namespace, + 'reference': reference, + 'profile': profile, + 'alg': alg, + }; +} + +class ConnectChallenge extends ConnectTarget { + final String nonce, domain, issuedAt, expiresAt; + final List requirements; + ConnectChallenge._( + super.origin, + super.requestId, + this.nonce, + this.domain, + this.issuedAt, + this.expiresAt, + List requirements, + ) : requirements = List.unmodifiable(requirements); + factory ConnectChallenge.fromJson( + Object? value, { + ConnectTarget? target, + DateTime? now, + }) { + final j = object(value, [ + 'version', + 'requestId', + 'nonce', + 'domain', + 'origin', + 'issuedAt', + 'expiresAt', + 'requirements', + ]); + if (j['version'] != 1 || j['version'] is! int) { + fail('unsupportedProtocolVersion'); + } + if (j['origin'] is! String || + j['domain'] is! String || + j['issuedAt'] is! String || + j['expiresAt'] is! String || + j['requirements'] is! List) { + fail('invalidChallenge'); + } + final r = j['requirements'] as List; + if (r.isEmpty || r.length > 32) fail('invalidChallenge'); + final c = ConnectChallenge._( + j['origin'], + identifier(j['requestId']), + identifier(j['nonce']), + j['domain'], + j['issuedAt'], + j['expiresAt'], + r.map(SigningRequirement.fromJson).toList(), + ); + if (c.domain != c.origin.substring(8) || + (target != null && + (target.origin != c.origin || target.requestId != c.requestId))) { + fail('domainMismatch'); + } + c.validateTime(now ?? DateTime.now()); + return c; + } + void validateTime(DateTime now) { + DateTime timestamp(String input) { + if (!matches(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z', input)) { + fail('invalidChallenge'); + } + final d = DateTime.tryParse(input); + if (d == null || d.toUtc().toIso8601String() != input) { + fail('invalidChallenge'); + } + return d; + } + + final issued = timestamp(issuedAt), expires = timestamp(expiresAt); + final ttl = expires.difference(issued).inMilliseconds; + if (ttl <= 0 || + ttl > 300000 || + issued.isAfter(now.add(const Duration(seconds: 30)))) { + fail('invalidChallenge'); + } + if (!expires.isAfter(now)) fail('expiredRequest'); + } + + bool accepts(SigningSelection s) => requirements.any((r) => r.accepts(s)); + Map toJson() => { + 'version': 1, + 'requestId': requestId, + 'nonce': nonce, + 'domain': domain, + 'origin': origin, + 'issuedAt': issuedAt, + 'expiresAt': expiresAt, + 'requirements': requirements.map((r) => r.toJson()).toList(), + }; +} + +String canonicalMessage(ConnectChallenge c, SigningSelection s) { + if (!c.accepts(s)) fail('unsupportedProfile'); + final a = s.account; + if (s.profile == 'ethereum-siwe') { + if (!matches(r'[1-9][0-9]{0,14}', a.reference) || + !matches(r'0x[0-9a-fA-F]{40}', a.address)) { + fail('invalidAccount'); + } + return '${c.domain} wants you to sign in with your Ethereum account:\n${a.address}\n\nApprove this Connect sign-in request.\n\nURI: ${c.origin}\nVersion: 1\nChain ID: ${a.reference}\nNonce: ${c.nonce}\nIssued At: ${c.issuedAt}\nExpiration Time: ${c.expiresAt}\nRequest ID: ${c.requestId}\nResources:\n- urn:connect:version:1\n- urn:connect:profile:${s.profile}\n- urn:connect:alg:${s.alg ?? 'none'}\n- urn:connect:account:${a.namespace}:${a.reference}:${a.address}'; + } + return [ + 'Connect Authentication', + 'Version: 1', + 'Domain: ${c.domain}', + 'URI: ${c.origin}', + 'Account: ${a.namespace}:${a.reference}:${a.address}', + 'Profile: ${s.profile}', + 'Algorithm: ${s.alg ?? 'none'}', + 'Nonce: ${c.nonce}', + 'Request ID: ${c.requestId}', + 'Issued At: ${c.issuedAt}', + 'Expiration Time: ${c.expiresAt}', + ].join('\n'); +} + +Uint8List canonicalBytes(ConnectChallenge c, SigningSelection s) => + Uint8List.fromList(utf8.encode(canonicalMessage(c, s))); diff --git a/lib/src/wallet.dart b/lib/src/wallet.dart new file mode 100644 index 0000000..776c0f6 --- /dev/null +++ b/lib/src/wallet.dart @@ -0,0 +1,279 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:http/http.dart' as http; +import 'protocol.dart'; +import 'handoff.dart'; + +class WalletSignature { + final String signature; + final String? publicKey; + const WalletSignature(this.signature, {this.publicKey}); +} + +abstract interface class WalletAccount { + SigningSelection get selection; + Future sign(Uint8List canonicalPayload); +} + +abstract interface class WalletAccountProvider { + Future> getAccounts(); +} + +abstract interface class ConnectNetwork { + Future challenge(ConnectTarget target); + Future approve(ConnectTarget target, Map proof); +} + +class HttpsNetwork implements ConnectNetwork { + final http.Client _client; + HttpsNetwork({http.Client? client}) : _client = client ?? http.Client(); + void close() => _client.close(); + Future _request( + ConnectTarget target, + String action, [ + Map? body, + ]) async { + validateOrigin(target.origin); + final url = Uri.parse( + '${target.origin}/api/auth/wallet-connect/$action${body == null ? '?requestId=${target.requestId}' : ''}', + ); + final abort = Completer(); + final request = http.AbortableRequest( + body == null ? 'GET' : 'POST', + url, + abortTrigger: abort.future, + )..followRedirects = false; + if (body != null) { + request.headers['Content-Type'] = 'application/json'; + request.body = jsonEncode(body); + } + try { + return await (() async { + final response = await _client.send(request); + if (response.statusCode != 200) { + await response.stream.listen((_) {}).cancel(); + fail('serverRejected'); + } + final bytes = BytesBuilder(copy: false); + await for (final chunk in response.stream) { + if (bytes.length + chunk.length > 16384) fail('malformedResponse'); + bytes.add(chunk); + } + return jsonDecode(utf8.decode(bytes.takeBytes())); + })().timeout( + const Duration(seconds: 15), + onTimeout: () { + if (!abort.isCompleted) abort.complete(); + fail('networkError'); + }, + ); + } on ConnectException { + rethrow; + } catch (_) { + fail('networkError'); + } finally { + if (!abort.isCompleted) abort.complete(); + } + } + + @override + Future challenge(ConnectTarget target) => + _request(target, 'challenge'); + @override + Future approve(ConnectTarget target, Map proof) => + _request(target, 'approve', proof); +} + +enum ConnectState { + awaitingUserApproval, + signing, + submitting, + approved, + responseReady, + rejected, + cancelled, + expired, + failed, +} + +class CompatibleAccount { + final SigningSelection selection; + final WalletAccount _signer; + CompatibleAccount._(this.selection, this._signer); +} + +class ConnectRequest { + final ConnectChallenge challenge; + final List compatibleAccounts; + final ConnectNetwork _network; + String? _response; + + /// Encrypted response to return by BLE, response QR, or another host channel. + String? get response => _response; + ConnectHandoff? get handoff => + _network is _HandoffNetwork ? _network.ticket : null; + final DateTime Function() _now; + final void Function(ConnectRequest) _changed; + ConnectState _state = ConnectState.awaitingUserApproval; + ConnectState get state => _state; + ConnectRequest._( + this.challenge, + List accounts, + this._network, + this._now, + this._changed, + ) : compatibleAccounts = List.unmodifiable( + accounts + .map((a) => CompatibleAccount._(a.selection, a)) + .where((a) => challenge.accepts(a.selection)), + ); + void _set(ConnectState state) { + _state = state; + _changed(this); + } + + void _pending() { + if (_state != ConnectState.awaitingUserApproval) fail('invalidState'); + } + + void cancel() { + if (_state != ConnectState.awaitingUserApproval && + _state != ConnectState.signing) { + fail('invalidState'); + } + _set(ConnectState.cancelled); + } + + /// Local rejection only: possession of a public QR must not allow remote denial. + void reject() { + _pending(); + _set(ConnectState.rejected); + } + + Future approve(CompatibleAccount account) async { + _pending(); + if (!compatibleAccounts.contains(account)) fail('noCompatibleAccount'); + try { + challenge.validateTime(_now()); + _set(ConnectState.signing); + final signature = await account._signer.sign( + canonicalBytes(challenge, account.selection), + ); + if (_state == ConnectState.cancelled) fail('cancelled'); + challenge.validateTime(_now()); + _set(ConnectState.submitting); + final result = await _network.approve(challenge, { + 'requestId': challenge.requestId, + 'account': account.selection.account.toJson(), + 'profile': account.selection.profile, + 'alg': account.selection.alg, + 'signature': signature.signature, + if (signature.publicKey != null) 'publicKey': signature.publicKey, + }); + if (result is! Map || + !['APPROVED', 'RESPONSE_READY'].contains(result['status']) || + result['requestId'] != challenge.requestId) { + fail('serverRejected'); + } + if (result['status'] == 'RESPONSE_READY') { + _response = result['response'] as String; + _set(ConnectState.responseReady); + } else { + _set(ConnectState.approved); + } + } catch (e) { + if (_state != ConnectState.cancelled) { + _set( + e is ConnectException && e.code == 'expiredRequest' + ? ConnectState.expired + : ConnectState.failed, + ); + } + if (e is ConnectException) rethrow; + fail('signingFailed'); + } + } +} + +class ConnectClient { + final WalletAccountProvider accountProvider; + final ConnectNetwork? network; + final ConnectTransport transport; + final DateTime Function() now; + final Set _handled = {}; + final _requests = StreamController.broadcast(); + Stream get requests => _requests.stream; + ConnectClient({ + required this.accountProvider, + this.network, + ConnectTransport? transport, + DateTime Function()? now, + }) : transport = transport ?? UriTransport(), + now = now ?? DateTime.now; + Future handleUri(String input) => resolve(input); + Future resolve(String input) { + if (input.contains('#')) { + final handoff = ConnectHandoff.parse(input, now: now); + return _resolve(handoff.challenge, _HandoffNetwork(handoff)); + } + return resolveTarget(transport.parse(input)); + } + + Future resolveTarget(ConnectTarget target) { + final adapter = network; + if (adapter == null) fail('transportUnavailable'); + return _resolve(target, adapter); + } + + Future _resolve( + ConnectTarget target, + ConnectNetwork adapter, + ) async { + if (_requests.isClosed) fail('disposed'); + final key = '${target.origin}/${target.requestId}'; + if (!_handled.add(key)) fail('replayedRequest'); + try { + final challenge = ConnectChallenge.fromJson( + await adapter.challenge(target), + target: target, + now: now(), + ); + final request = ConnectRequest._( + challenge, + await accountProvider.getAccounts(), + adapter, + now, + (r) { + if (!_requests.isClosed) _requests.add(r); + }, + ); + if (request.compatibleAccounts.isEmpty) fail('noCompatibleAccount'); + if (_requests.isClosed) fail('disposed'); + _requests.add(request); + return request; + } catch (_) { + _handled.remove(key); + rethrow; + } + } + + Future dispose() => _requests.close(); +} + +class _HandoffNetwork implements ConnectNetwork { + final ConnectHandoff ticket; + _HandoffNetwork(this.ticket); + @override + Future challenge(ConnectTarget target) async => + ticket.challenge.toJson(); + @override + Future approve( + ConnectTarget target, + Map proof, + ) async => { + 'requestId': target.requestId, + 'status': 'RESPONSE_READY', + 'response': await ticket.sealProof(proof), + }; +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..4e57061 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,20 @@ +name: flutter_connect +description: Wallet-side Connect protocol with strict transport validation, explicit approval, and multi-chain signing adapters. +version: 0.1.0 +homepage: https://github.com/bchainhub/flutter_connect +repository: https://github.com/bchainhub/flutter_connect +issue_tracker: https://github.com/bchainhub/flutter_connect/issues +environment: + sdk: '>=3.9.0 <4.0.0' + flutter: '>=3.35.0' +dependencies: + flutter: + sdk: flutter + http: ^1.5.0 + app_links: ^6.4.1 + bluetooth_low_energy: ^6.2.1 + cryptography: ^2.9.0 +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 diff --git a/test/bluetooth_test.dart b/test/bluetooth_test.dart new file mode 100644 index 0000000..08ce0aa --- /dev/null +++ b/test/bluetooth_test.dart @@ -0,0 +1,93 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:bluetooth_low_energy/bluetooth_low_energy.dart'; +import 'package:flutter_connect/flutter_connect.dart'; +import 'connect_test.dart' show vectors; + +class FakePeripheral implements PeripheralManager { + int added = 0, removed = 0, advertised = 0, stopped = 0; + @override + BluetoothLowEnergyState get state => BluetoothLowEnergyState.poweredOn; + @override + Stream + get characteristicWriteRequested => const Stream.empty(); + @override + Stream + get characteristicReadRequested => const Stream.empty(); + @override + Future addService(GATTService service) async { + added++; + expect(service.characteristics.length, 2); + } + + @override + Future removeService(GATTService service) async { + removed++; + } + + @override + Future startAdvertising(Advertisement advertisement) async { + advertised++; + } + + @override + Future stopAdvertising() async { + stopped++; + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + test('native peripheral owns and cleans up its service', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + addTearDown(() => debugDefaultTargetPlatformOverride = null); + final challenge = Map.from( + vectors().first['challenge'] as Map, + ); + challenge['issuedAt'] = DateTime.now() + .toUtc() + .toIso8601String() + .replaceFirst(RegExp(r'\d{6}Z$'), '000Z'); + challenge['expiresAt'] = DateTime.now() + .toUtc() + .add(const Duration(minutes: 2)) + .toIso8601String() + .replaceFirst(RegExp(r'\d{6}Z$'), '000Z'); + final payload = base64Url + .encode( + utf8.encode( + jsonEncode({ + 'type': 'connect:handoff:1', + 'challenge': challenge, + 'key': '01' * 32, + }), + ), + ) + .replaceAll('=', ''); + final handoff = ConnectHandoff.parse( + 'connect://example.com/connect/v1/${challenge['requestId']}#$payload', + ); + final manager = FakePeripheral(); + final errors = []; + final peripheral = ConnectBluetoothPeripheral( + handoff: handoff, + manager: manager, + onError: errors.add, + ); + await peripheral.start(); + expect(manager.added, 1); + expect(manager.advertised, 1); + await expectLater(peripheral.start(), throwsA(isA())); + expect(() => peripheral.publish('{}'), throwsA(isA())); + await peripheral.stop(); + await peripheral.stop(); + expect(manager.removed, 1); + expect(manager.stopped, 1); + expect(errors, isEmpty); + }); +} diff --git a/test/chains_test.dart b/test/chains_test.dart new file mode 100644 index 0000000..ce44c0c --- /dev/null +++ b/test/chains_test.dart @@ -0,0 +1,51 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:flutter_connect/flutter_connect.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('chain catalog agrees with shared proofs and signing requirements', () { + final vectors = + jsonDecode(File('test/fixtures/conformance.json').readAsStringSync()) + as List; + for (final chain in ConnectChains.all) { + final v = vectors.firstWhere( + (v) => + v['proof']['profile'] == chain.profile && + v['proof']['account']['reference'] == chain.reference, + ); + final selection = chain.account( + v['proof']['account']['address'] as String, + ); + expect(chain.requirement.accepts(selection), true); + final challenge = ConnectChallenge.fromJson( + v['challenge'], + now: DateTime.parse(v['challenge']['issuedAt']), + ); + expect(canonicalMessage(challenge, selection), v['canonical']); + } + expect( + requirementsFor([ConnectChains.solana, ConnectChains.solana]), + hasLength(1), + ); + expect(ConnectChains.all, hasLength(16)); + }); + test('ordinary wallet adapter preserves canonical bytes and proof', () async { + final bytes = Uint8List.fromList(utf8.encode('Connect authentication')); + final account = ChainWalletAccount( + chain: ConnectChains.solana, + address: 'testaddress', + sign: (received) async { + expect(received, bytes); + return const WalletSignature('signature'); + }, + ); + expect(account.selection.profile, 'solana-ed25519'); + expect((await account.sign(bytes)).signature, 'signature'); + final custom = ConnectChain.evm('Arbitrum', 42161); + expect(custom.reference, '42161'); + expect(custom.profile, 'ethereum-siwe'); + expect(() => ConnectChain.evm('bad', 0), throwsA(isA())); + }); +} diff --git a/test/connect_test.dart b/test/connect_test.dart new file mode 100644 index 0000000..3e1f41d --- /dev/null +++ b/test/connect_test.dart @@ -0,0 +1,237 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_connect/flutter_connect.dart'; +import 'package:cryptography/cryptography.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; + +List vectors() => + jsonDecode(File('test/fixtures/conformance.json').readAsStringSync()) + as List; +Uint8List unhex(String s) => Uint8List.fromList([ + for (var i = 0; i < s.length; i += 2) + int.parse(s.substring(i, i + 2), radix: 16), +]); +String hex(List b) => + b.map((i) => i.toRadixString(16).padLeft(2, '0')).join(); +final now = DateTime.parse('2026-09-07T10:00:00.000Z'); +SigningSelection selection(Map p) => SigningSelection( + account: WalletIdentity.fromJson(p['account']), + profile: p['profile'] as String, + alg: p['alg'] as int?, +); + +class Signer implements WalletAccount { + @override + final SigningSelection selection; + final Future Function(Uint8List) callback; + Signer(this.selection, this.callback); + @override + Future sign(Uint8List payload) => callback(payload); +} + +class Provider implements WalletAccountProvider { + final List accounts; + Provider(this.accounts); + @override + Future> getAccounts() async => accounts; +} + +class Network implements ConnectNetwork { + final Map data; + int submitted = 0; + Object? response; + Network(this.data); + @override + Future challenge(ConnectTarget t) async => data; + @override + Future approve(ConnectTarget t, Map p) async { + submitted++; + return response ?? {'requestId': t.requestId, 'status': 'APPROVED'}; + } +} + +void main() { + for (final dynamic item in vectors()) { + final v = item as Map; + test('shared canonical bytes ${v['name']}', () async { + final c = ConnectChallenge.fromJson(v['challenge'], now: now), + s = selection(v['proof'] as Map); + expect(canonicalMessage(c, s), v['canonical']); + expect(hex(canonicalBytes(c, s)), v['canonicalHex']); + if (v['name'] == 'raw-ed25519') { + final algorithm = Ed25519(), + key = await Ed25519().newKeyPairFromSeed( + unhex(v['testSeed'] as String), + ); + final signature = await algorithm.sign( + canonicalBytes(c, s), + keyPair: key, + ); + expect(hex(signature.bytes), (v['proof'] as Map)['signature']); + expect( + await algorithm.verify(canonicalBytes(c, s), signature: signature), + isTrue, + ); + } + }); + } + test('strict URI transports', () { + final parser = UriTransport(), + uri = 'connect://example.com/connect/v1/${'a' * 64}'; + expect( + parser.parse(uri).origin, + parser.parse(uri.replaceFirst('connect:', 'https:')).origin, + ); + for (final bad in [ + '$uri\n', + '$uri?redeemSecret=secret', + '$uri#fragment', + '$uri/', + uri.replaceFirst('v1', 'v2'), + uri.replaceFirst('example.com', 'user@example.com'), + uri.replaceFirst('example.com', 'EXAMPLE.com'), + uri.replaceFirst('example.com', '127.0.0.1'), + uri.replaceFirst('example.com', 'example.com:443'), + uri.replaceFirst('example.com', 'example.com.'), + uri.replaceFirst('/connect/', '/x/../connect/'), + uri.replaceFirst('connect:', 'http:'), + ]) { + expect( + () => parser.parse(bad), + throwsA(isA()), + reason: bad, + ); + } + }); + test('challenge binding, strict schema, times and expiry', () { + final raw = vectors().first['challenge'] as Map, + target = ConnectTarget('https://example.com', 'a' * 64); + for (final patch in [ + {'domain': 'evil.com'}, + {'origin': 'https://evil.com'}, + {'requestId': 'c' * 64}, + {'version': 2}, + {'nonce': '${'a' * 64}\n'}, + {'issuedAt': '2026-02-30T10:00:00.000Z'}, + {'expiresAt': now.toIso8601String()}, + {'redeemSecret': 'secret'}, + ]) { + expect( + () => ConnectChallenge.fromJson( + {...raw, ...patch}, + target: target, + now: now, + ), + throwsA(isA()), + ); + } + }); + test( + 'explicit approval, local reject, replay and server confirmation', + () async { + final v = vectors()[1], data = v['challenge'] as Map; + var calls = 0; + final signer = Signer(selection(v['proof'] as Map), ( + p, + ) async { + calls++; + return const WalletSignature('00'); + }), + network = Network(data); + final client = ConnectClient( + accountProvider: Provider([signer]), + network: network, + now: () => now, + ); + final request = await client.resolve( + ConnectTarget(data['origin'], data['requestId']).connectUri, + ); + expect(calls, 0); + await request.approve(request.compatibleAccounts.first); + expect(request.state, ConnectState.approved); + expect(calls, 1); + await expectLater( + request.approve(request.compatibleAccounts.first), + throwsA(isA()), + ); + await expectLater( + client.resolve(request.challenge.connectUri), + throwsA(isA()), + ); + await client.dispose(); + }, + ); + test('cancel during signing prevents submission', () async { + final v = vectors()[1], + result = Completer(), + network = Network(v['challenge'] as Map); + final signer = Signer( + selection(v['proof'] as Map), + (_) => result.future, + ); + final client = ConnectClient( + accountProvider: Provider([signer]), + network: network, + now: () => now, + ); + final request = await client.resolve( + ConnectTarget('https://example.com', 'a' * 64).connectUri, + ); + final approval = request.approve(request.compatibleAccounts.first); + request.cancel(); + result.complete(const WalletSignature('00')); + await expectLater(approval, throwsA(isA())); + expect(network.submitted, 0); + expect(request.state, ConnectState.cancelled); + await client.dispose(); + }); + test('expiry checked again after signing', () async { + final v = vectors()[1], + network = Network(v['challenge'] as Map); + var time = now; + final signer = Signer(selection(v['proof'] as Map), ( + _, + ) async { + time = now.add(const Duration(minutes: 5)); + return const WalletSignature('00'); + }); + final client = ConnectClient( + accountProvider: Provider([signer]), + network: network, + now: () => time, + ); + final request = await client.resolve( + ConnectTarget('https://example.com', 'a' * 64).connectUri, + ); + await expectLater( + request.approve(request.compatibleAccounts.first), + throwsA(isA()), + ); + expect(network.submitted, 0); + expect(request.state, ConnectState.expired); + await client.dispose(); + }); + test('HTTP redirect rejected and response size bounded', () async { + final target = ConnectTarget('https://example.com', 'a' * 64); + for (final response in [ + http.Response('', 302), + http.Response('x' * 20000, 200), + ]) { + final network = HttpsNetwork( + client: MockClient((r) async { + expect(r.followRedirects, isFalse); + return response; + }), + ); + await expectLater( + network.challenge(target), + throwsA(isA()), + ); + network.close(); + } + }); +} diff --git a/test/fixtures/conformance.json b/test/fixtures/conformance.json new file mode 100644 index 0000000..5996af5 --- /dev/null +++ b/test/fixtures/conformance.json @@ -0,0 +1,1829 @@ +[ + { + "name": "xcb-ed448", + "challenge": { + "version": 1, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nonce": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "domain": "example.com", + "origin": "https://example.com", + "issuedAt": "2026-09-07T10:00:00.000Z", + "expiresAt": "2026-09-07T10:02:00.000Z", + "requirements": [ + { + "namespace": "core", + "reference": "1", + "profile": "xcb-ed448", + "alg": -53 + }, + { + "namespace": "raw", + "reference": "ed25519", + "profile": "raw-ed25519", + "alg": -19 + }, + { + "namespace": "eip155", + "reference": "1", + "profile": "ethereum-siwe", + "alg": null + }, + { + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93", + "profile": "bitcoin-bip322-p2wpkh", + "alg": null + } + ] + }, + "proof": { + "account": { + "namespace": "core", + "reference": "1", + "address": "cb23f5e89ba978317da861b7ad06ba3103d41c876098" + }, + "profile": "xcb-ed448", + "alg": -53, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "publicKey": "e0758a33267939a394fb5ccb202ee851cebc2e89c91ac1289e2bfcddfd9ff9fc5694b0f569d7f7e9da16e1cde9301b29f48128b3cbd1168580", + "signature": "143d579070c935d46a28952a77589bd531f83f34636d71acfba8b4e8e4b1c39de5b1addb807be9d0e54d625de95959d2f32514669ae1c24a80acc763bafe645b0fe1c094d4812c5d29035e1a1c52356f6b8538f550dd2731acf247232af37364c2c34f5218076f257b1b75d2f701febf1c00" + }, + "canonical": "Connect Authentication\nVersion: 1\nDomain: example.com\nURI: https://example.com\nAccount: core:1:cb23f5e89ba978317da861b7ad06ba3103d41c876098\nProfile: xcb-ed448\nAlgorithm: -53\nNonce: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nRequest ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nIssued At: 2026-09-07T10:00:00.000Z\nExpiration Time: 2026-09-07T10:02:00.000Z", + "canonicalHex": "436f6e6e6563742041757468656e7469636174696f6e0a56657273696f6e3a20310a446f6d61696e3a206578616d706c652e636f6d0a5552493a2068747470733a2f2f6578616d706c652e636f6d0a4163636f756e743a20636f72653a313a63623233663565383962613937383331376461383631623761643036626133313033643431633837363039380a50726f66696c653a207863622d65643434380a416c676f726974686d3a202d35330a4e6f6e63653a20626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262620a526571756573742049443a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610a4973737565642041743a20323032362d30392d30375431303a30303a30302e3030305a0a45787069726174696f6e2054696d653a20323032362d30392d30375431303a30323a30302e3030305a", + "testSeed": "010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101", + "valid": true + }, + { + "name": "raw-ed25519", + "challenge": { + "version": 1, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nonce": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "domain": "example.com", + "origin": "https://example.com", + "issuedAt": "2026-09-07T10:00:00.000Z", + "expiresAt": "2026-09-07T10:02:00.000Z", + "requirements": [ + { + "namespace": "core", + "reference": "1", + "profile": "xcb-ed448", + "alg": -53 + }, + { + "namespace": "raw", + "reference": "ed25519", + "profile": "raw-ed25519", + "alg": -19 + }, + { + "namespace": "eip155", + "reference": "1", + "profile": "ethereum-siwe", + "alg": null + }, + { + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93", + "profile": "bitcoin-bip322-p2wpkh", + "alg": null + } + ] + }, + "proof": { + "account": { + "namespace": "raw", + "reference": "ed25519", + "address": "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c" + }, + "profile": "raw-ed25519", + "alg": -19, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "publicKey": "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c", + "signature": "9d711214892c0d6fb909b3b2c6434a13350244335921a0df612d1dcf9c2ba64288d3232b4e1f902b122c6a294f5a47aa81f62cfa1189f675c9875b799713f801" + }, + "canonical": "Connect Authentication\nVersion: 1\nDomain: example.com\nURI: https://example.com\nAccount: raw:ed25519:8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c\nProfile: raw-ed25519\nAlgorithm: -19\nNonce: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nRequest ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nIssued At: 2026-09-07T10:00:00.000Z\nExpiration Time: 2026-09-07T10:02:00.000Z", + "canonicalHex": "436f6e6e6563742041757468656e7469636174696f6e0a56657273696f6e3a20310a446f6d61696e3a206578616d706c652e636f6d0a5552493a2068747470733a2f2f6578616d706c652e636f6d0a4163636f756e743a207261773a656432353531393a386138386533646437343039663139356664353264623264336362613564373263613637303962663164393431323162663337343838303162343066366635630a50726f66696c653a207261772d656432353531390a416c676f726974686d3a202d31390a4e6f6e63653a20626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262620a526571756573742049443a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610a4973737565642041743a20323032362d30392d30375431303a30303a30302e3030305a0a45787069726174696f6e2054696d653a20323032362d30392d30375431303a30323a30302e3030305a", + "testSeed": "0101010101010101010101010101010101010101010101010101010101010101", + "valid": true + }, + { + "name": "ethereum-siwe", + "challenge": { + "version": 1, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nonce": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "domain": "example.com", + "origin": "https://example.com", + "issuedAt": "2026-09-07T10:00:00.000Z", + "expiresAt": "2026-09-07T10:02:00.000Z", + "requirements": [ + { + "namespace": "core", + "reference": "1", + "profile": "xcb-ed448", + "alg": -53 + }, + { + "namespace": "raw", + "reference": "ed25519", + "profile": "raw-ed25519", + "alg": -19 + }, + { + "namespace": "eip155", + "reference": "1", + "profile": "ethereum-siwe", + "alg": null + }, + { + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93", + "profile": "bitcoin-bip322-p2wpkh", + "alg": null + } + ] + }, + "proof": { + "account": { + "namespace": "eip155", + "reference": "1", + "address": "0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1" + }, + "profile": "ethereum-siwe", + "alg": null, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "signature": "0xec4ab3e1544ebf537ff3bb1ea22d5ce078789aa09f41801271a7d089b3ce9e5b15db95d4896a9e684c6bd0c97548634a28630ded8b48fe2ee22735620c63c0571c" + }, + "canonical": "example.com wants you to sign in with your Ethereum account:\n0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1\n\nApprove this Connect sign-in request.\n\nURI: https://example.com\nVersion: 1\nChain ID: 1\nNonce: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nIssued At: 2026-09-07T10:00:00.000Z\nExpiration Time: 2026-09-07T10:02:00.000Z\nRequest ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nResources:\n- urn:connect:version:1\n- urn:connect:profile:ethereum-siwe\n- urn:connect:alg:none\n- urn:connect:account:eip155:1:0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1", + "canonicalHex": "6578616d706c652e636f6d2077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078316136343266304533633361463534354537416342443338623037323531423339393039313446310a0a417070726f7665207468697320436f6e6e656374207369676e2d696e20726571756573742e0a0a5552493a2068747470733a2f2f6578616d706c652e636f6d0a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a20626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262620a4973737565642041743a20323032362d30392d30375431303a30303a30302e3030305a0a45787069726174696f6e2054696d653a20323032362d30392d30375431303a30323a30302e3030305a0a526571756573742049443a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610a5265736f75726365733a0a2d2075726e3a636f6e6e6563743a76657273696f6e3a310a2d2075726e3a636f6e6e6563743a70726f66696c653a657468657265756d2d736977650a2d2075726e3a636f6e6e6563743a616c673a6e6f6e650a2d2075726e3a636f6e6e6563743a6163636f756e743a6569703135353a313a307831613634326630453363336146353435453741634244333862303732353142333939303931344631", + "valid": true + }, + { + "name": "bitcoin-bip322-p2wpkh", + "challenge": { + "version": 1, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nonce": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "domain": "example.com", + "origin": "https://example.com", + "issuedAt": "2026-09-07T10:00:00.000Z", + "expiresAt": "2026-09-07T10:02:00.000Z", + "requirements": [ + { + "namespace": "core", + "reference": "1", + "profile": "xcb-ed448", + "alg": -53 + }, + { + "namespace": "raw", + "reference": "ed25519", + "profile": "raw-ed25519", + "alg": -19 + }, + { + "namespace": "eip155", + "reference": "1", + "profile": "ethereum-siwe", + "alg": null + }, + { + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93", + "profile": "bitcoin-bip322-p2wpkh", + "alg": null + } + ] + }, + "proof": { + "account": { + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93", + "address": "bc1q0xcqpzrky6eff2g52qdye53xkk9jxkvrh6yhyw" + }, + "profile": "bitcoin-bip322-p2wpkh", + "alg": null, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "signature": "AkcwRAIgCKKfedpfI8vEagm9Z3PH1zTGm71UDNVFcAF/KqT+SVcCIHfvlJdFGCIsyp9Sf5PnwWk07Wv8xEUn/YT1H2Xo+8y0ASEDG4TFVnsSZECZXT7VqroFZdceGDRgSBn/nBf16dXdB48=" + }, + "canonical": "Connect Authentication\nVersion: 1\nDomain: example.com\nURI: https://example.com\nAccount: bip122:000000000019d6689c085ae165831e93:bc1q0xcqpzrky6eff2g52qdye53xkk9jxkvrh6yhyw\nProfile: bitcoin-bip322-p2wpkh\nAlgorithm: none\nNonce: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nRequest ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nIssued At: 2026-09-07T10:00:00.000Z\nExpiration Time: 2026-09-07T10:02:00.000Z", + "canonicalHex": "436f6e6e6563742041757468656e7469636174696f6e0a56657273696f6e3a20310a446f6d61696e3a206578616d706c652e636f6d0a5552493a2068747470733a2f2f6578616d706c652e636f6d0a4163636f756e743a206269703132323a30303030303030303030313964363638396330383561653136353833316539333a6263317130786371707a726b793665666632673532716479653533786b6b396a786b76726836796879770a50726f66696c653a20626974636f696e2d6269703332322d703277706b680a416c676f726974686d3a206e6f6e650a4e6f6e63653a20626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262620a526571756573742049443a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610a4973737565642041743a20323032362d30392d30375431303a30303a30302e3030305a0a45787069726174696f6e2054696d653a20323032362d30392d30375431303a30323a30302e3030305a", + "valid": true + }, + { + "name": "Polygon", + "challenge": { + "version": 1, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nonce": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "domain": "example.com", + "origin": "https://example.com", + "issuedAt": "2026-09-07T10:00:00.000Z", + "expiresAt": "2026-09-07T10:02:00.000Z", + "requirements": [ + { + "profile": "xcb-ed448", + "alg": -53, + "namespace": "core", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "137" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "8453" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "56" + }, + { + "profile": "bitcoin-bip322-p2wpkh", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "solana-ed25519", + "alg": -19, + "namespace": "solana", + "reference": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + }, + { + "profile": "tron-signmessage-v2", + "alg": null, + "namespace": "tron", + "reference": "728126428" + }, + { + "profile": "monero-spend-v2", + "alg": null, + "namespace": "monero", + "reference": "418015bb9ae982a1975da7d79277c270" + }, + { + "profile": "stellar-sep53", + "alg": -19, + "namespace": "stellar", + "reference": "pubnet" + }, + { + "profile": "litecoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "12a765e31ffd4059bada1e25190f6e98" + }, + { + "profile": "xrpl-secp256k1", + "alg": null, + "namespace": "xrpl", + "reference": "0" + }, + { + "profile": "zcash-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "00040fe8ec8471911baa1db1266ea15d" + }, + { + "profile": "cardano-cip8", + "alg": -19, + "namespace": "cip34", + "reference": "1-764824073" + }, + { + "profile": "bitcoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "xrpl-ed25519", + "alg": -19, + "namespace": "xrpl", + "reference": "0" + } + ] + }, + "proof": { + "account": { + "namespace": "eip155", + "reference": "137", + "address": "0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1" + }, + "profile": "ethereum-siwe", + "alg": null, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "signature": "0x6edace48e7ae196e8f3a1dd82472e8b5f453fb856d213443d70848ffe38ba0480f2b9ddba1652b10b3b8ce644158d1c43b0da61e47e5132071df11a2774a47fe1b" + }, + "canonical": "example.com wants you to sign in with your Ethereum account:\n0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1\n\nApprove this Connect sign-in request.\n\nURI: https://example.com\nVersion: 1\nChain ID: 137\nNonce: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nIssued At: 2026-09-07T10:00:00.000Z\nExpiration Time: 2026-09-07T10:02:00.000Z\nRequest ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nResources:\n- urn:connect:version:1\n- urn:connect:profile:ethereum-siwe\n- urn:connect:alg:none\n- urn:connect:account:eip155:137:0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1", + "canonicalHex": "6578616d706c652e636f6d2077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078316136343266304533633361463534354537416342443338623037323531423339393039313446310a0a417070726f7665207468697320436f6e6e656374207369676e2d696e20726571756573742e0a0a5552493a2068747470733a2f2f6578616d706c652e636f6d0a56657273696f6e3a20310a436861696e2049443a203133370a4e6f6e63653a20626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262620a4973737565642041743a20323032362d30392d30375431303a30303a30302e3030305a0a45787069726174696f6e2054696d653a20323032362d30392d30375431303a30323a30302e3030305a0a526571756573742049443a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610a5265736f75726365733a0a2d2075726e3a636f6e6e6563743a76657273696f6e3a310a2d2075726e3a636f6e6e6563743a70726f66696c653a657468657265756d2d736977650a2d2075726e3a636f6e6e6563743a616c673a6e6f6e650a2d2075726e3a636f6e6e6563743a6163636f756e743a6569703135353a3133373a307831613634326630453363336146353435453741634244333862303732353142333939303931344631", + "valid": true + }, + { + "name": "Base", + "challenge": { + "version": 1, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nonce": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "domain": "example.com", + "origin": "https://example.com", + "issuedAt": "2026-09-07T10:00:00.000Z", + "expiresAt": "2026-09-07T10:02:00.000Z", + "requirements": [ + { + "profile": "xcb-ed448", + "alg": -53, + "namespace": "core", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "137" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "8453" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "56" + }, + { + "profile": "bitcoin-bip322-p2wpkh", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "solana-ed25519", + "alg": -19, + "namespace": "solana", + "reference": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + }, + { + "profile": "tron-signmessage-v2", + "alg": null, + "namespace": "tron", + "reference": "728126428" + }, + { + "profile": "monero-spend-v2", + "alg": null, + "namespace": "monero", + "reference": "418015bb9ae982a1975da7d79277c270" + }, + { + "profile": "stellar-sep53", + "alg": -19, + "namespace": "stellar", + "reference": "pubnet" + }, + { + "profile": "litecoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "12a765e31ffd4059bada1e25190f6e98" + }, + { + "profile": "xrpl-secp256k1", + "alg": null, + "namespace": "xrpl", + "reference": "0" + }, + { + "profile": "zcash-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "00040fe8ec8471911baa1db1266ea15d" + }, + { + "profile": "cardano-cip8", + "alg": -19, + "namespace": "cip34", + "reference": "1-764824073" + }, + { + "profile": "bitcoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "xrpl-ed25519", + "alg": -19, + "namespace": "xrpl", + "reference": "0" + } + ] + }, + "proof": { + "account": { + "namespace": "eip155", + "reference": "8453", + "address": "0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1" + }, + "profile": "ethereum-siwe", + "alg": null, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "signature": "0x77909f3f8eef0684981d519f53bc82aa6911fd49bab9f7c25a536c0f9230348e43736cb75ec9bedcbdfa48c34826a9b9741ad6bace449364da1925a33206e2fd1b" + }, + "canonical": "example.com wants you to sign in with your Ethereum account:\n0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1\n\nApprove this Connect sign-in request.\n\nURI: https://example.com\nVersion: 1\nChain ID: 8453\nNonce: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nIssued At: 2026-09-07T10:00:00.000Z\nExpiration Time: 2026-09-07T10:02:00.000Z\nRequest ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nResources:\n- urn:connect:version:1\n- urn:connect:profile:ethereum-siwe\n- urn:connect:alg:none\n- urn:connect:account:eip155:8453:0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1", + "canonicalHex": "6578616d706c652e636f6d2077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078316136343266304533633361463534354537416342443338623037323531423339393039313446310a0a417070726f7665207468697320436f6e6e656374207369676e2d696e20726571756573742e0a0a5552493a2068747470733a2f2f6578616d706c652e636f6d0a56657273696f6e3a20310a436861696e2049443a20383435330a4e6f6e63653a20626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262620a4973737565642041743a20323032362d30392d30375431303a30303a30302e3030305a0a45787069726174696f6e2054696d653a20323032362d30392d30375431303a30323a30302e3030305a0a526571756573742049443a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610a5265736f75726365733a0a2d2075726e3a636f6e6e6563743a76657273696f6e3a310a2d2075726e3a636f6e6e6563743a70726f66696c653a657468657265756d2d736977650a2d2075726e3a636f6e6e6563743a616c673a6e6f6e650a2d2075726e3a636f6e6e6563743a6163636f756e743a6569703135353a383435333a307831613634326630453363336146353435453741634244333862303732353142333939303931344631", + "valid": true + }, + { + "name": "BNB Smart Chain", + "challenge": { + "version": 1, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nonce": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "domain": "example.com", + "origin": "https://example.com", + "issuedAt": "2026-09-07T10:00:00.000Z", + "expiresAt": "2026-09-07T10:02:00.000Z", + "requirements": [ + { + "profile": "xcb-ed448", + "alg": -53, + "namespace": "core", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "137" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "8453" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "56" + }, + { + "profile": "bitcoin-bip322-p2wpkh", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "solana-ed25519", + "alg": -19, + "namespace": "solana", + "reference": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + }, + { + "profile": "tron-signmessage-v2", + "alg": null, + "namespace": "tron", + "reference": "728126428" + }, + { + "profile": "monero-spend-v2", + "alg": null, + "namespace": "monero", + "reference": "418015bb9ae982a1975da7d79277c270" + }, + { + "profile": "stellar-sep53", + "alg": -19, + "namespace": "stellar", + "reference": "pubnet" + }, + { + "profile": "litecoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "12a765e31ffd4059bada1e25190f6e98" + }, + { + "profile": "xrpl-secp256k1", + "alg": null, + "namespace": "xrpl", + "reference": "0" + }, + { + "profile": "zcash-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "00040fe8ec8471911baa1db1266ea15d" + }, + { + "profile": "cardano-cip8", + "alg": -19, + "namespace": "cip34", + "reference": "1-764824073" + }, + { + "profile": "bitcoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "xrpl-ed25519", + "alg": -19, + "namespace": "xrpl", + "reference": "0" + } + ] + }, + "proof": { + "account": { + "namespace": "eip155", + "reference": "56", + "address": "0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1" + }, + "profile": "ethereum-siwe", + "alg": null, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "signature": "0x72cc3a6639abcd8a22862c77daaaba4979be8056df2a95fe73fd049a28d9e5286ea99fe03f589a57b7855131bd6cf21e33ba440bbd860fd1cc8e8e29ee5ac5b01c" + }, + "canonical": "example.com wants you to sign in with your Ethereum account:\n0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1\n\nApprove this Connect sign-in request.\n\nURI: https://example.com\nVersion: 1\nChain ID: 56\nNonce: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nIssued At: 2026-09-07T10:00:00.000Z\nExpiration Time: 2026-09-07T10:02:00.000Z\nRequest ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nResources:\n- urn:connect:version:1\n- urn:connect:profile:ethereum-siwe\n- urn:connect:alg:none\n- urn:connect:account:eip155:56:0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1", + "canonicalHex": "6578616d706c652e636f6d2077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078316136343266304533633361463534354537416342443338623037323531423339393039313446310a0a417070726f7665207468697320436f6e6e656374207369676e2d696e20726571756573742e0a0a5552493a2068747470733a2f2f6578616d706c652e636f6d0a56657273696f6e3a20310a436861696e2049443a2035360a4e6f6e63653a20626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262620a4973737565642041743a20323032362d30392d30375431303a30303a30302e3030305a0a45787069726174696f6e2054696d653a20323032362d30392d30375431303a30323a30302e3030305a0a526571756573742049443a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610a5265736f75726365733a0a2d2075726e3a636f6e6e6563743a76657273696f6e3a310a2d2075726e3a636f6e6e6563743a70726f66696c653a657468657265756d2d736977650a2d2075726e3a636f6e6e6563743a616c673a6e6f6e650a2d2075726e3a636f6e6e6563743a6163636f756e743a6569703135353a35363a307831613634326630453363336146353435453741634244333862303732353142333939303931344631", + "valid": true + }, + { + "name": "Solana", + "challenge": { + "version": 1, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nonce": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "domain": "example.com", + "origin": "https://example.com", + "issuedAt": "2026-09-07T10:00:00.000Z", + "expiresAt": "2026-09-07T10:02:00.000Z", + "requirements": [ + { + "profile": "xcb-ed448", + "alg": -53, + "namespace": "core", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "137" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "8453" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "56" + }, + { + "profile": "bitcoin-bip322-p2wpkh", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "solana-ed25519", + "alg": -19, + "namespace": "solana", + "reference": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + }, + { + "profile": "tron-signmessage-v2", + "alg": null, + "namespace": "tron", + "reference": "728126428" + }, + { + "profile": "monero-spend-v2", + "alg": null, + "namespace": "monero", + "reference": "418015bb9ae982a1975da7d79277c270" + }, + { + "profile": "stellar-sep53", + "alg": -19, + "namespace": "stellar", + "reference": "pubnet" + }, + { + "profile": "litecoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "12a765e31ffd4059bada1e25190f6e98" + }, + { + "profile": "xrpl-secp256k1", + "alg": null, + "namespace": "xrpl", + "reference": "0" + }, + { + "profile": "zcash-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "00040fe8ec8471911baa1db1266ea15d" + }, + { + "profile": "cardano-cip8", + "alg": -19, + "namespace": "cip34", + "reference": "1-764824073" + }, + { + "profile": "bitcoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "xrpl-ed25519", + "alg": -19, + "namespace": "xrpl", + "reference": "0" + } + ] + }, + "proof": { + "account": { + "namespace": "solana", + "reference": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "address": "AKnL4NNf3DGWZJS6cPknBuEGnVsV4A4m5tgebLHaRSZ9" + }, + "profile": "solana-ed25519", + "alg": -19, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "signature": "32b1340bd39579a44d2f91d38a09d91a6ff1047cff14672e06dcbc935aeefeba1c6bfd98001d043296c0fb3a927746f2fe417ca3d7ad0c10ca79cfe43a2adf00" + }, + "canonical": "Connect Authentication\nVersion: 1\nDomain: example.com\nURI: https://example.com\nAccount: solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:AKnL4NNf3DGWZJS6cPknBuEGnVsV4A4m5tgebLHaRSZ9\nProfile: solana-ed25519\nAlgorithm: -19\nNonce: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nRequest ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nIssued At: 2026-09-07T10:00:00.000Z\nExpiration Time: 2026-09-07T10:02:00.000Z", + "canonicalHex": "436f6e6e6563742041757468656e7469636174696f6e0a56657273696f6e3a20310a446f6d61696e3a206578616d706c652e636f6d0a5552493a2068747470733a2f2f6578616d706c652e636f6d0a4163636f756e743a20736f6c616e613a3565796b7434557346763850384e4a64545245705931767a714b715a4b7664703a414b6e4c344e4e66334447575a4a533663506b6e427545476e5673563441346d35746765624c486152535a390a50726f66696c653a20736f6c616e612d656432353531390a416c676f726974686d3a202d31390a4e6f6e63653a20626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262620a526571756573742049443a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610a4973737565642041743a20323032362d30392d30375431303a30303a30302e3030305a0a45787069726174696f6e2054696d653a20323032362d30392d30375431303a30323a30302e3030305a", + "valid": true + }, + { + "name": "TRON", + "challenge": { + "version": 1, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nonce": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "domain": "example.com", + "origin": "https://example.com", + "issuedAt": "2026-09-07T10:00:00.000Z", + "expiresAt": "2026-09-07T10:02:00.000Z", + "requirements": [ + { + "profile": "xcb-ed448", + "alg": -53, + "namespace": "core", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "137" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "8453" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "56" + }, + { + "profile": "bitcoin-bip322-p2wpkh", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "solana-ed25519", + "alg": -19, + "namespace": "solana", + "reference": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + }, + { + "profile": "tron-signmessage-v2", + "alg": null, + "namespace": "tron", + "reference": "728126428" + }, + { + "profile": "monero-spend-v2", + "alg": null, + "namespace": "monero", + "reference": "418015bb9ae982a1975da7d79277c270" + }, + { + "profile": "stellar-sep53", + "alg": -19, + "namespace": "stellar", + "reference": "pubnet" + }, + { + "profile": "litecoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "12a765e31ffd4059bada1e25190f6e98" + }, + { + "profile": "xrpl-secp256k1", + "alg": null, + "namespace": "xrpl", + "reference": "0" + }, + { + "profile": "zcash-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "00040fe8ec8471911baa1db1266ea15d" + }, + { + "profile": "cardano-cip8", + "alg": -19, + "namespace": "cip34", + "reference": "1-764824073" + }, + { + "profile": "bitcoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "xrpl-ed25519", + "alg": -19, + "namespace": "xrpl", + "reference": "0" + } + ] + }, + "proof": { + "account": { + "namespace": "tron", + "reference": "728126428", + "address": "TCNkawTmcQgYSU8nP8cHswT1QPjharxJr7" + }, + "profile": "tron-signmessage-v2", + "alg": null, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "signature": "0xa2c88cc0652ee7ee9d868a9a6a29d8657c11578050047475747c1da905cbab5f45422425ee5d7292ca0d7d5c3dbca06f31110c12dd855bfdd1b54bbdf6656d0e1b" + }, + "canonical": "Connect Authentication\nVersion: 1\nDomain: example.com\nURI: https://example.com\nAccount: tron:728126428:TCNkawTmcQgYSU8nP8cHswT1QPjharxJr7\nProfile: tron-signmessage-v2\nAlgorithm: none\nNonce: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nRequest ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nIssued At: 2026-09-07T10:00:00.000Z\nExpiration Time: 2026-09-07T10:02:00.000Z", + "canonicalHex": "436f6e6e6563742041757468656e7469636174696f6e0a56657273696f6e3a20310a446f6d61696e3a206578616d706c652e636f6d0a5552493a2068747470733a2f2f6578616d706c652e636f6d0a4163636f756e743a2074726f6e3a3732383132363432383a54434e6b6177546d635167595355386e503863487377543151506a686172784a72370a50726f66696c653a2074726f6e2d7369676e6d6573736167652d76320a416c676f726974686d3a206e6f6e650a4e6f6e63653a20626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262620a526571756573742049443a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610a4973737565642041743a20323032362d30392d30375431303a30303a30302e3030305a0a45787069726174696f6e2054696d653a20323032362d30392d30375431303a30323a30302e3030305a", + "valid": true + }, + { + "name": "Monero", + "challenge": { + "version": 1, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nonce": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "domain": "example.com", + "origin": "https://example.com", + "issuedAt": "2026-09-07T10:00:00.000Z", + "expiresAt": "2026-09-07T10:02:00.000Z", + "requirements": [ + { + "profile": "xcb-ed448", + "alg": -53, + "namespace": "core", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "137" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "8453" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "56" + }, + { + "profile": "bitcoin-bip322-p2wpkh", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "solana-ed25519", + "alg": -19, + "namespace": "solana", + "reference": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + }, + { + "profile": "tron-signmessage-v2", + "alg": null, + "namespace": "tron", + "reference": "728126428" + }, + { + "profile": "monero-spend-v2", + "alg": null, + "namespace": "monero", + "reference": "418015bb9ae982a1975da7d79277c270" + }, + { + "profile": "stellar-sep53", + "alg": -19, + "namespace": "stellar", + "reference": "pubnet" + }, + { + "profile": "litecoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "12a765e31ffd4059bada1e25190f6e98" + }, + { + "profile": "xrpl-secp256k1", + "alg": null, + "namespace": "xrpl", + "reference": "0" + }, + { + "profile": "zcash-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "00040fe8ec8471911baa1db1266ea15d" + }, + { + "profile": "cardano-cip8", + "alg": -19, + "namespace": "cip34", + "reference": "1-764824073" + }, + { + "profile": "bitcoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "xrpl-ed25519", + "alg": -19, + "namespace": "xrpl", + "reference": "0" + } + ] + }, + "proof": { + "account": { + "namespace": "monero", + "reference": "418015bb9ae982a1975da7d79277c270", + "address": "44yQXfkWZNmJ8QgRfFWTzmJ8QgRfFWTzmJ8QgRfFWTzmJ9HskYAEKgjVy4kd3K4MaxERLtGa7FFrVNLF3jYWyjHCFA3tNHB" + }, + "profile": "monero-spend-v2", + "alg": null, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "signature": "SigV2RBp4qz9y95naAds3unpCzng6iAKY8MM37a9z5usjye6RQCefRve4veebYzZ5b3gYnJJWPCwjW8BTrCYVy1utVVPG" + }, + "canonical": "Connect Authentication\nVersion: 1\nDomain: example.com\nURI: https://example.com\nAccount: monero:418015bb9ae982a1975da7d79277c270:44yQXfkWZNmJ8QgRfFWTzmJ8QgRfFWTzmJ8QgRfFWTzmJ9HskYAEKgjVy4kd3K4MaxERLtGa7FFrVNLF3jYWyjHCFA3tNHB\nProfile: monero-spend-v2\nAlgorithm: none\nNonce: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nRequest ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nIssued At: 2026-09-07T10:00:00.000Z\nExpiration Time: 2026-09-07T10:02:00.000Z", + "canonicalHex": "436f6e6e6563742041757468656e7469636174696f6e0a56657273696f6e3a20310a446f6d61696e3a206578616d706c652e636f6d0a5552493a2068747470733a2f2f6578616d706c652e636f6d0a4163636f756e743a206d6f6e65726f3a34313830313562623961653938326131393735646137643739323737633237303a3434795158666b575a4e6d4a38516752664657547a6d4a38516752664657547a6d4a38516752664657547a6d4a3948736b5941454b676a5679346b64334b344d617845524c74476137464672564e4c46336a5957796a4843464133744e48420a50726f66696c653a206d6f6e65726f2d7370656e642d76320a416c676f726974686d3a206e6f6e650a4e6f6e63653a20626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262620a526571756573742049443a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610a4973737565642041743a20323032362d30392d30375431303a30303a30302e3030305a0a45787069726174696f6e2054696d653a20323032362d30392d30375431303a30323a30302e3030305a", + "valid": true + }, + { + "name": "Stellar", + "challenge": { + "version": 1, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nonce": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "domain": "example.com", + "origin": "https://example.com", + "issuedAt": "2026-09-07T10:00:00.000Z", + "expiresAt": "2026-09-07T10:02:00.000Z", + "requirements": [ + { + "profile": "xcb-ed448", + "alg": -53, + "namespace": "core", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "137" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "8453" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "56" + }, + { + "profile": "bitcoin-bip322-p2wpkh", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "solana-ed25519", + "alg": -19, + "namespace": "solana", + "reference": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + }, + { + "profile": "tron-signmessage-v2", + "alg": null, + "namespace": "tron", + "reference": "728126428" + }, + { + "profile": "monero-spend-v2", + "alg": null, + "namespace": "monero", + "reference": "418015bb9ae982a1975da7d79277c270" + }, + { + "profile": "stellar-sep53", + "alg": -19, + "namespace": "stellar", + "reference": "pubnet" + }, + { + "profile": "litecoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "12a765e31ffd4059bada1e25190f6e98" + }, + { + "profile": "xrpl-secp256k1", + "alg": null, + "namespace": "xrpl", + "reference": "0" + }, + { + "profile": "zcash-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "00040fe8ec8471911baa1db1266ea15d" + }, + { + "profile": "cardano-cip8", + "alg": -19, + "namespace": "cip34", + "reference": "1-764824073" + }, + { + "profile": "bitcoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "xrpl-ed25519", + "alg": -19, + "namespace": "xrpl", + "reference": "0" + } + ] + }, + "proof": { + "account": { + "namespace": "stellar", + "reference": "pubnet", + "address": "GCFIRY65OQE7DFP5KLNS2PF2LVZMUZYJX4OZIEQ36N2IQANUB5XVYOJR" + }, + "profile": "stellar-sep53", + "alg": -19, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "signature": "6e97ca22f5d1a88fa4563d50cac9dfa7ef58dbbb0ace420aac4388aaf10b51aa4265b3583e26c3c674a63fada018e051317bb1c9022af67480826b9bcfccae0b" + }, + "canonical": "Connect Authentication\nVersion: 1\nDomain: example.com\nURI: https://example.com\nAccount: stellar:pubnet:GCFIRY65OQE7DFP5KLNS2PF2LVZMUZYJX4OZIEQ36N2IQANUB5XVYOJR\nProfile: stellar-sep53\nAlgorithm: -19\nNonce: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nRequest ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nIssued At: 2026-09-07T10:00:00.000Z\nExpiration Time: 2026-09-07T10:02:00.000Z", + "canonicalHex": "436f6e6e6563742041757468656e7469636174696f6e0a56657273696f6e3a20310a446f6d61696e3a206578616d706c652e636f6d0a5552493a2068747470733a2f2f6578616d706c652e636f6d0a4163636f756e743a207374656c6c61723a7075626e65743a47434649525936354f514537444650354b4c4e53325046324c565a4d555a594a58344f5a49455133364e324951414e5542355856594f4a520a50726f66696c653a207374656c6c61722d73657035330a416c676f726974686d3a202d31390a4e6f6e63653a20626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262620a526571756573742049443a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610a4973737565642041743a20323032362d30392d30375431303a30303a30302e3030305a0a45787069726174696f6e2054696d653a20323032362d30392d30375431303a30323a30302e3030305a", + "valid": true + }, + { + "name": "Litecoin", + "challenge": { + "version": 1, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nonce": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "domain": "example.com", + "origin": "https://example.com", + "issuedAt": "2026-09-07T10:00:00.000Z", + "expiresAt": "2026-09-07T10:02:00.000Z", + "requirements": [ + { + "profile": "xcb-ed448", + "alg": -53, + "namespace": "core", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "137" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "8453" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "56" + }, + { + "profile": "bitcoin-bip322-p2wpkh", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "solana-ed25519", + "alg": -19, + "namespace": "solana", + "reference": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + }, + { + "profile": "tron-signmessage-v2", + "alg": null, + "namespace": "tron", + "reference": "728126428" + }, + { + "profile": "monero-spend-v2", + "alg": null, + "namespace": "monero", + "reference": "418015bb9ae982a1975da7d79277c270" + }, + { + "profile": "stellar-sep53", + "alg": -19, + "namespace": "stellar", + "reference": "pubnet" + }, + { + "profile": "litecoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "12a765e31ffd4059bada1e25190f6e98" + }, + { + "profile": "xrpl-secp256k1", + "alg": null, + "namespace": "xrpl", + "reference": "0" + }, + { + "profile": "zcash-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "00040fe8ec8471911baa1db1266ea15d" + }, + { + "profile": "cardano-cip8", + "alg": -19, + "namespace": "cip34", + "reference": "1-764824073" + }, + { + "profile": "bitcoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "xrpl-ed25519", + "alg": -19, + "namespace": "xrpl", + "reference": "0" + } + ] + }, + "proof": { + "account": { + "namespace": "bip122", + "reference": "12a765e31ffd4059bada1e25190f6e98", + "address": "LWKNsGErA9XxsrKVPimDAbuRXjCyyazZtc" + }, + "profile": "litecoin-signmessage", + "alg": null, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "signature": "H3ncdVReM2oEiTYg3y/c8T7HoeqRD+wcxZLN2pDYwfOiRxJHoxDv6KiWloFGm0zNiRdsoctU67qCkRrtUwTaI/U=" + }, + "canonical": "Connect Authentication\nVersion: 1\nDomain: example.com\nURI: https://example.com\nAccount: bip122:12a765e31ffd4059bada1e25190f6e98:LWKNsGErA9XxsrKVPimDAbuRXjCyyazZtc\nProfile: litecoin-signmessage\nAlgorithm: none\nNonce: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nRequest ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nIssued At: 2026-09-07T10:00:00.000Z\nExpiration Time: 2026-09-07T10:02:00.000Z", + "canonicalHex": "436f6e6e6563742041757468656e7469636174696f6e0a56657273696f6e3a20310a446f6d61696e3a206578616d706c652e636f6d0a5552493a2068747470733a2f2f6578616d706c652e636f6d0a4163636f756e743a206269703132323a31326137363565333166666434303539626164613165323531393066366539383a4c574b4e734745724139587873724b5650696d4441627552586a437979617a5a74630a50726f66696c653a206c697465636f696e2d7369676e6d6573736167650a416c676f726974686d3a206e6f6e650a4e6f6e63653a20626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262620a526571756573742049443a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610a4973737565642041743a20323032362d30392d30375431303a30303a30302e3030305a0a45787069726174696f6e2054696d653a20323032362d30392d30375431303a30323a30302e3030305a", + "valid": true + }, + { + "name": "XRP Ledger", + "challenge": { + "version": 1, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nonce": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "domain": "example.com", + "origin": "https://example.com", + "issuedAt": "2026-09-07T10:00:00.000Z", + "expiresAt": "2026-09-07T10:02:00.000Z", + "requirements": [ + { + "profile": "xcb-ed448", + "alg": -53, + "namespace": "core", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "137" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "8453" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "56" + }, + { + "profile": "bitcoin-bip322-p2wpkh", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "solana-ed25519", + "alg": -19, + "namespace": "solana", + "reference": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + }, + { + "profile": "tron-signmessage-v2", + "alg": null, + "namespace": "tron", + "reference": "728126428" + }, + { + "profile": "monero-spend-v2", + "alg": null, + "namespace": "monero", + "reference": "418015bb9ae982a1975da7d79277c270" + }, + { + "profile": "stellar-sep53", + "alg": -19, + "namespace": "stellar", + "reference": "pubnet" + }, + { + "profile": "litecoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "12a765e31ffd4059bada1e25190f6e98" + }, + { + "profile": "xrpl-secp256k1", + "alg": null, + "namespace": "xrpl", + "reference": "0" + }, + { + "profile": "zcash-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "00040fe8ec8471911baa1db1266ea15d" + }, + { + "profile": "cardano-cip8", + "alg": -19, + "namespace": "cip34", + "reference": "1-764824073" + }, + { + "profile": "bitcoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "xrpl-ed25519", + "alg": -19, + "namespace": "xrpl", + "reference": "0" + } + ] + }, + "proof": { + "account": { + "namespace": "xrpl", + "reference": "0", + "address": "rMPrYipfRHJryWfwYARAwhsVGvHwpUDjgA" + }, + "profile": "xrpl-secp256k1", + "alg": null, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "signature": "304402207c3047b906662a362b87e267d3deef6274437c56bbe9ca2970ef47573ca9d718022051b30802c6796870632ea421126dc1b1480969418c34fa5e4d524d596fd1b749", + "publicKey": "035a7fa22521397be0ffe9cd7c35dde0fb0c8ea96e48ff84e0c6309d0d4d70d9ea" + }, + "canonical": "Connect Authentication\nVersion: 1\nDomain: example.com\nURI: https://example.com\nAccount: xrpl:0:rMPrYipfRHJryWfwYARAwhsVGvHwpUDjgA\nProfile: xrpl-secp256k1\nAlgorithm: none\nNonce: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nRequest ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nIssued At: 2026-09-07T10:00:00.000Z\nExpiration Time: 2026-09-07T10:02:00.000Z", + "canonicalHex": "436f6e6e6563742041757468656e7469636174696f6e0a56657273696f6e3a20310a446f6d61696e3a206578616d706c652e636f6d0a5552493a2068747470733a2f2f6578616d706c652e636f6d0a4163636f756e743a207872706c3a303a724d50725969706652484a72795766775941524177687356477648777055446a67410a50726f66696c653a207872706c2d736563703235366b310a416c676f726974686d3a206e6f6e650a4e6f6e63653a20626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262620a526571756573742049443a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610a4973737565642041743a20323032362d30392d30375431303a30303a30302e3030305a0a45787069726174696f6e2054696d653a20323032362d30392d30375431303a30323a30302e3030305a", + "valid": true + }, + { + "name": "Zcash", + "challenge": { + "version": 1, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nonce": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "domain": "example.com", + "origin": "https://example.com", + "issuedAt": "2026-09-07T10:00:00.000Z", + "expiresAt": "2026-09-07T10:02:00.000Z", + "requirements": [ + { + "profile": "xcb-ed448", + "alg": -53, + "namespace": "core", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "137" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "8453" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "56" + }, + { + "profile": "bitcoin-bip322-p2wpkh", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "solana-ed25519", + "alg": -19, + "namespace": "solana", + "reference": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + }, + { + "profile": "tron-signmessage-v2", + "alg": null, + "namespace": "tron", + "reference": "728126428" + }, + { + "profile": "monero-spend-v2", + "alg": null, + "namespace": "monero", + "reference": "418015bb9ae982a1975da7d79277c270" + }, + { + "profile": "stellar-sep53", + "alg": -19, + "namespace": "stellar", + "reference": "pubnet" + }, + { + "profile": "litecoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "12a765e31ffd4059bada1e25190f6e98" + }, + { + "profile": "xrpl-secp256k1", + "alg": null, + "namespace": "xrpl", + "reference": "0" + }, + { + "profile": "zcash-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "00040fe8ec8471911baa1db1266ea15d" + }, + { + "profile": "cardano-cip8", + "alg": -19, + "namespace": "cip34", + "reference": "1-764824073" + }, + { + "profile": "bitcoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "xrpl-ed25519", + "alg": -19, + "namespace": "xrpl", + "reference": "0" + } + ] + }, + "proof": { + "account": { + "namespace": "bip122", + "reference": "00040fe8ec8471911baa1db1266ea15d", + "address": "t1Uy2cPMA3p5WDggEA1b32PwaaB2nfDssvp" + }, + "profile": "zcash-signmessage", + "alg": null, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "signature": "IFUKutZSC710SyDQ25+apzr4ABN3KiMPI88PWWRdkAL7R8cmIcEqU7RyZtTu3fYtBDYUjRm1UOiL9+CZKfuF0xE=" + }, + "canonical": "Connect Authentication\nVersion: 1\nDomain: example.com\nURI: https://example.com\nAccount: bip122:00040fe8ec8471911baa1db1266ea15d:t1Uy2cPMA3p5WDggEA1b32PwaaB2nfDssvp\nProfile: zcash-signmessage\nAlgorithm: none\nNonce: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nRequest ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nIssued At: 2026-09-07T10:00:00.000Z\nExpiration Time: 2026-09-07T10:02:00.000Z", + "canonicalHex": "436f6e6e6563742041757468656e7469636174696f6e0a56657273696f6e3a20310a446f6d61696e3a206578616d706c652e636f6d0a5552493a2068747470733a2f2f6578616d706c652e636f6d0a4163636f756e743a206269703132323a30303034306665386563383437313931316261613164623132363665613135643a743155793263504d41337035574467674541316233325077616142326e6644737376700a50726f66696c653a207a636173682d7369676e6d6573736167650a416c676f726974686d3a206e6f6e650a4e6f6e63653a20626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262620a526571756573742049443a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610a4973737565642041743a20323032362d30392d30375431303a30303a30302e3030305a0a45787069726174696f6e2054696d653a20323032362d30392d30375431303a30323a30302e3030305a", + "valid": true + }, + { + "name": "Cardano", + "challenge": { + "version": 1, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nonce": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "domain": "example.com", + "origin": "https://example.com", + "issuedAt": "2026-09-07T10:00:00.000Z", + "expiresAt": "2026-09-07T10:02:00.000Z", + "requirements": [ + { + "profile": "xcb-ed448", + "alg": -53, + "namespace": "core", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "137" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "8453" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "56" + }, + { + "profile": "bitcoin-bip322-p2wpkh", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "solana-ed25519", + "alg": -19, + "namespace": "solana", + "reference": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + }, + { + "profile": "tron-signmessage-v2", + "alg": null, + "namespace": "tron", + "reference": "728126428" + }, + { + "profile": "monero-spend-v2", + "alg": null, + "namespace": "monero", + "reference": "418015bb9ae982a1975da7d79277c270" + }, + { + "profile": "stellar-sep53", + "alg": -19, + "namespace": "stellar", + "reference": "pubnet" + }, + { + "profile": "litecoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "12a765e31ffd4059bada1e25190f6e98" + }, + { + "profile": "xrpl-secp256k1", + "alg": null, + "namespace": "xrpl", + "reference": "0" + }, + { + "profile": "zcash-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "00040fe8ec8471911baa1db1266ea15d" + }, + { + "profile": "cardano-cip8", + "alg": -19, + "namespace": "cip34", + "reference": "1-764824073" + }, + { + "profile": "bitcoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "xrpl-ed25519", + "alg": -19, + "namespace": "xrpl", + "reference": "0" + } + ] + }, + "proof": { + "account": { + "namespace": "cip34", + "reference": "1-764824073", + "address": "addr1vyxk54m7j3q6mrkevcunryrwf4p7e68c93cjk8gzxkhlkpsjpczl2" + }, + "profile": "cardano-cip8", + "alg": -19, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "signature": "84582aa201276761646472657373581d610d6a577e9441ad8ed9663931906e4d43ece8f82c712b1d0235affb06a166686173686564f45901ac436f6e6e6563742041757468656e7469636174696f6e0a56657273696f6e3a20310a446f6d61696e3a206578616d706c652e636f6d0a5552493a2068747470733a2f2f6578616d706c652e636f6d0a4163636f756e743a2063697033343a312d3736343832343037333a61646472317679786b35346d376a3371366d726b657663756e7279727766347037653638633933636a6b38677a786b686c6b70736a70637a6c320a50726f66696c653a2063617264616e6f2d636970380a416c676f726974686d3a202d31390a4e6f6e63653a20626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262620a526571756573742049443a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610a4973737565642041743a20323032362d30392d30375431303a30303a30302e3030305a0a45787069726174696f6e2054696d653a20323032362d30392d30375431303a30323a30302e3030305a5840e8f2cae247740ea10649edf235f1c112efa54e84df2125ffba0f608b0e7cb896f5018199bfa196ce3756fdbb0ac9c193176ff851055713a3d2c194ca3fc44203", + "publicKey": "a40101032720062158208a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c" + }, + "canonical": "Connect Authentication\nVersion: 1\nDomain: example.com\nURI: https://example.com\nAccount: cip34:1-764824073:addr1vyxk54m7j3q6mrkevcunryrwf4p7e68c93cjk8gzxkhlkpsjpczl2\nProfile: cardano-cip8\nAlgorithm: -19\nNonce: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nRequest ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nIssued At: 2026-09-07T10:00:00.000Z\nExpiration Time: 2026-09-07T10:02:00.000Z", + "canonicalHex": "436f6e6e6563742041757468656e7469636174696f6e0a56657273696f6e3a20310a446f6d61696e3a206578616d706c652e636f6d0a5552493a2068747470733a2f2f6578616d706c652e636f6d0a4163636f756e743a2063697033343a312d3736343832343037333a61646472317679786b35346d376a3371366d726b657663756e7279727766347037653638633933636a6b38677a786b686c6b70736a70637a6c320a50726f66696c653a2063617264616e6f2d636970380a416c676f726974686d3a202d31390a4e6f6e63653a20626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262620a526571756573742049443a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610a4973737565642041743a20323032362d30392d30375431303a30303a30302e3030305a0a45787069726174696f6e2054696d653a20323032362d30392d30375431303a30323a30302e3030305a", + "valid": true + }, + { + "name": "Bitcoin (legacy P2PKH)", + "challenge": { + "version": 1, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nonce": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "domain": "example.com", + "origin": "https://example.com", + "issuedAt": "2026-09-07T10:00:00.000Z", + "expiresAt": "2026-09-07T10:02:00.000Z", + "requirements": [ + { + "profile": "xcb-ed448", + "alg": -53, + "namespace": "core", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "137" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "8453" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "56" + }, + { + "profile": "bitcoin-bip322-p2wpkh", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "solana-ed25519", + "alg": -19, + "namespace": "solana", + "reference": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + }, + { + "profile": "tron-signmessage-v2", + "alg": null, + "namespace": "tron", + "reference": "728126428" + }, + { + "profile": "monero-spend-v2", + "alg": null, + "namespace": "monero", + "reference": "418015bb9ae982a1975da7d79277c270" + }, + { + "profile": "stellar-sep53", + "alg": -19, + "namespace": "stellar", + "reference": "pubnet" + }, + { + "profile": "litecoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "12a765e31ffd4059bada1e25190f6e98" + }, + { + "profile": "xrpl-secp256k1", + "alg": null, + "namespace": "xrpl", + "reference": "0" + }, + { + "profile": "zcash-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "00040fe8ec8471911baa1db1266ea15d" + }, + { + "profile": "cardano-cip8", + "alg": -19, + "namespace": "cip34", + "reference": "1-764824073" + }, + { + "profile": "bitcoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "xrpl-ed25519", + "alg": -19, + "namespace": "xrpl", + "reference": "0" + } + ] + }, + "proof": { + "account": { + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93", + "address": "1C6Rc3w25VHud3dLDamutaqfKWqhrLRTaD" + }, + "profile": "bitcoin-signmessage", + "alg": null, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "signature": "IDcmKBr0cwOhdmuYIIcrdlInNQ/KI6+9Kax4G/Yb0EzOfDgOTAMEl1bQI2kzC9402voJAav94MjWZaUB1bBrFRY=" + }, + "canonical": "Connect Authentication\nVersion: 1\nDomain: example.com\nURI: https://example.com\nAccount: bip122:000000000019d6689c085ae165831e93:1C6Rc3w25VHud3dLDamutaqfKWqhrLRTaD\nProfile: bitcoin-signmessage\nAlgorithm: none\nNonce: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nRequest ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nIssued At: 2026-09-07T10:00:00.000Z\nExpiration Time: 2026-09-07T10:02:00.000Z", + "canonicalHex": "436f6e6e6563742041757468656e7469636174696f6e0a56657273696f6e3a20310a446f6d61696e3a206578616d706c652e636f6d0a5552493a2068747470733a2f2f6578616d706c652e636f6d0a4163636f756e743a206269703132323a30303030303030303030313964363638396330383561653136353833316539333a3143365263337732355648756433644c44616d75746171664b577168724c525461440a50726f66696c653a20626974636f696e2d7369676e6d6573736167650a416c676f726974686d3a206e6f6e650a4e6f6e63653a20626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262620a526571756573742049443a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610a4973737565642041743a20323032362d30392d30375431303a30303a30302e3030305a0a45787069726174696f6e2054696d653a20323032362d30392d30375431303a30323a30302e3030305a", + "valid": true + }, + { + "name": "XRP Ledger (Ed25519)", + "challenge": { + "version": 1, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nonce": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "domain": "example.com", + "origin": "https://example.com", + "issuedAt": "2026-09-07T10:00:00.000Z", + "expiresAt": "2026-09-07T10:02:00.000Z", + "requirements": [ + { + "profile": "xcb-ed448", + "alg": -53, + "namespace": "core", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "1" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "137" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "8453" + }, + { + "profile": "ethereum-siwe", + "alg": null, + "namespace": "eip155", + "reference": "56" + }, + { + "profile": "bitcoin-bip322-p2wpkh", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "solana-ed25519", + "alg": -19, + "namespace": "solana", + "reference": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + }, + { + "profile": "tron-signmessage-v2", + "alg": null, + "namespace": "tron", + "reference": "728126428" + }, + { + "profile": "monero-spend-v2", + "alg": null, + "namespace": "monero", + "reference": "418015bb9ae982a1975da7d79277c270" + }, + { + "profile": "stellar-sep53", + "alg": -19, + "namespace": "stellar", + "reference": "pubnet" + }, + { + "profile": "litecoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "12a765e31ffd4059bada1e25190f6e98" + }, + { + "profile": "xrpl-secp256k1", + "alg": null, + "namespace": "xrpl", + "reference": "0" + }, + { + "profile": "zcash-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "00040fe8ec8471911baa1db1266ea15d" + }, + { + "profile": "cardano-cip8", + "alg": -19, + "namespace": "cip34", + "reference": "1-764824073" + }, + { + "profile": "bitcoin-signmessage", + "alg": null, + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93" + }, + { + "profile": "xrpl-ed25519", + "alg": -19, + "namespace": "xrpl", + "reference": "0" + } + ] + }, + "proof": { + "account": { + "namespace": "xrpl", + "reference": "0", + "address": "r3sNTMefq5gsRumMYsNznnX6yzzxVH6dTC" + }, + "profile": "xrpl-ed25519", + "alg": -19, + "requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "signature": "d68225ada67d42a9183d09d0858553590023102ceefa4fbf73a7835f6a35ab280b2dd65004f40ee558545c4dcbccc962763183d2c904307fd85cbb8573f3db04", + "publicKey": "ed06895bec3fde4090f06d840770d888d49e3089b3757c4285e3851bc33964e0f9" + }, + "canonical": "Connect Authentication\nVersion: 1\nDomain: example.com\nURI: https://example.com\nAccount: xrpl:0:r3sNTMefq5gsRumMYsNznnX6yzzxVH6dTC\nProfile: xrpl-ed25519\nAlgorithm: -19\nNonce: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nRequest ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nIssued At: 2026-09-07T10:00:00.000Z\nExpiration Time: 2026-09-07T10:02:00.000Z", + "canonicalHex": "436f6e6e6563742041757468656e7469636174696f6e0a56657273696f6e3a20310a446f6d61696e3a206578616d706c652e636f6d0a5552493a2068747470733a2f2f6578616d706c652e636f6d0a4163636f756e743a207872706c3a303a7233734e544d65667135677352756d4d59734e7a6e6e5836797a7a785648366454430a50726f66696c653a207872706c2d656432353531390a416c676f726974686d3a202d31390a4e6f6e63653a20626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262620a526571756573742049443a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610a4973737565642041743a20323032362d30392d30375431303a30303a30302e3030305a0a45787069726174696f6e2054696d653a20323032362d30392d30375431303a30323a30302e3030305a", + "valid": true + } +] diff --git a/test/handoff_test.dart b/test/handoff_test.dart new file mode 100644 index 0000000..9c86d21 --- /dev/null +++ b/test/handoff_test.dart @@ -0,0 +1,95 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_connect/flutter_connect.dart'; +import 'connect_test.dart' show vectors, now, Provider, Signer, selection; + +void main() { + final responses = []; + for (final dynamic value in vectors()) { + final v = value as Map; + test('offline approval and encrypted response ${v['name']}', () async { + final c = v['challenge'] as Map; + final proof = v['proof'] as Map; + final payload = base64Url + .encode( + utf8.encode( + jsonEncode({ + 'type': 'connect:handoff:1', + 'challenge': c, + 'key': '01' * 32, + }), + ), + ) + .replaceAll('=', ''); + final uri = + 'connect://${c['domain']}/connect/v1/${c['requestId']}#$payload'; + var signed = 0; + final client = ConnectClient( + now: () => now, + accountProvider: Provider([ + Signer(selection(proof), (bytes) async { + signed++; + expect(utf8.decode(bytes), v['canonical']); + return WalletSignature( + proof['signature'] as String, + publicKey: proof['publicKey'] as String?, + ); + }), + ]), + ); + final request = await client.resolve(uri); + expect(signed, 0); + expect(request.handoff, isNotNull); + await request.approve(request.compatibleAccounts.first); + expect(signed, 1); + expect(request.state, ConnectState.responseReady); + expect(request.response, isNotNull); + responses.add(request.response!); + await expectLater( + request.approve(request.compatibleAccounts.first), + throwsA(isA()), + ); + expect( + () => ConnectHandoff.parse( + uri, + expectedOrigin: 'https://evil.example', + now: () => now, + ), + throwsA(isA()), + ); + expect( + () => ConnectHandoff.parse( + uri, + now: () => now.add(const Duration(minutes: 10)), + ), + throwsA(isA()), + ); + }); + } + test('BLE frames are bounded and reconstruct the complete packet', () { + final buffer = ConnectBleResponseBuffer(); + expect(buffer.frame(0), Uint8List(8)); + final input = 'encrypted response ' * 100; + buffer.publish(input); + final result = []; + while (result.length < input.length) { + final frame = buffer.frame(result.length); + expect(frame.length, lessThanOrEqualTo(20)); + final header = ByteData.sublistView(frame); + expect(header.getUint32(0, Endian.little), input.length); + expect(header.getUint32(4, Endian.little), result.length); + result.addAll(frame.sublist(8)); + } + expect(utf8.decode(result), input); + expect(() => buffer.frame(-1), throwsA(isA())); + expect(() => buffer.publish('x' * 32769), throwsA(isA())); + buffer.clear(); + expect(buffer.frame(0), Uint8List(8)); + }); + tearDownAll(() { + final path = Platform.environment['CONNECT_DART_RESPONSES']; + if (path != null) File(path).writeAsStringSync(jsonEncode(responses)); + }); +} diff --git a/test/links_test.dart b/test/links_test.dart new file mode 100644 index 0000000..4cabcc7 --- /dev/null +++ b/test/links_test.dart @@ -0,0 +1,77 @@ +import 'dart:async'; +import 'package:app_links/app_links.dart'; +import 'package:flutter_connect/flutter_connect.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'connect_test.dart' + show Provider, Signer, Network, selection, vectors, now; + +class FakeLinks implements AppLinks { + final String initial; + final controller = StreamController.broadcast(); + FakeLinks(this.initial); + @override + Future getInitialLinkString() async => initial; + @override + Stream get stringLinkStream => controller.stream; + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class LinkNetwork extends Network { + LinkNetwork(super.data); + @override + Future challenge(ConnectTarget t) async => { + ...data, + 'requestId': t.requestId, + }; +} + +void main() { + test( + 'cold/warm raw links, duplicate delivery, errors and disposal', + () async { + final v = vectors()[1] as Map; + var signed = 0; + final wallet = Signer(selection(v['proof'] as Map), ( + _, + ) async { + signed++; + return const WalletSignature('00'); + }); + final client = ConnectClient( + accountProvider: Provider([wallet]), + network: LinkNetwork(v['challenge'] as Map), + now: () => now, + ); + final initial = ConnectTarget('https://example.com', 'a' * 64).connectUri; + final fake = FakeLinks(initial), errors = [], seen = []; + final subscription = client.requests.listen( + (r) => seen.add(r.challenge.requestId), + ); + final links = ConnectLinks( + client: client, + onError: errors.add, + links: fake, + ); + await links.start(); + fake.controller.add(initial); + fake.controller.add( + ConnectTarget('https://example.com', 'c' * 64).connectUri, + ); + await Future.delayed(Duration.zero); + expect(seen, ['a' * 64, 'c' * 64]); + expect(errors, isEmpty); + expect(signed, 0); + fake.controller.add('$initial\n'); + await Future.delayed(Duration.zero); + expect(errors.single, isA()); + await links.dispose(); + fake.controller.add('invalid'); + await Future.delayed(Duration.zero); + expect(errors.length, 1); + await subscription.cancel(); + await client.dispose(); + await fake.controller.close(); + }, + ); +} diff --git a/test/server_interop_test.dart b/test/server_interop_test.dart new file mode 100644 index 0000000..f446911 --- /dev/null +++ b/test/server_interop_test.dart @@ -0,0 +1,155 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:cryptography/cryptography.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_connect/flutter_connect.dart'; +import 'connect_test.dart' + show Provider, Signer, hex, unhex, vectors, selection; + +// Integration-only adapter: production HttpsNetwork never permits plaintext. +class LocalTestNetwork implements ConnectNetwork { + final String endpoint; + final _client = HttpClient(); + LocalTestNetwork(this.endpoint); + Future> call( + String action, [ + Map? body, + ]) async { + final request = await _client.openUrl( + body == null ? 'GET' : 'POST', + Uri.parse('$endpoint/$action'), + ); + request.headers.set('origin', 'https://example.com'); + if (body != null) { + request.headers.contentType = ContentType.json; + request.write(jsonEncode(body)); + } + final response = await request.close(); + final data = + jsonDecode(await utf8.decoder.bind(response).join()) + as Map; + if (response.statusCode != 200) { + throw StateError('Server status ${response.statusCode}: $data'); + } + return data; + } + + @override + Future challenge(ConnectTarget t) => + call('challenge?requestId=${t.requestId}'); + @override + Future approve(ConnectTarget t, Map proof) => + call('approve', proof); + void close() => _client.close(force: true); +} + +Future nativeEd448( + Uint8List message, + Map vector, +) async { + final dir = await Directory.systemTemp.createTemp('connect-ed448-test-'); + try { + // Public deterministic test seed, encoded as RFC8410 PKCS#8 for OpenSSL. + final seed = vector['testSeed'] as String; + await File( + '${dir.path}/key.der', + ).writeAsBytes(unhex('3047020100300506032b6571043b0439$seed')); + await File('${dir.path}/message').writeAsBytes(message); + final result = await Process.run('openssl', [ + 'pkeyutl', + '-sign', + '-rawin', + '-keyform', + 'DER', + '-inkey', + '${dir.path}/key.der', + '-in', + '${dir.path}/message', + '-out', + '${dir.path}/signature', + ]); + if (result.exitCode != 0) { + throw StateError('OpenSSL Ed448 bridge failed: ${result.stderr}'); + } + return WalletSignature( + hex(await File('${dir.path}/signature').readAsBytes()), + publicKey: (vector['proof'] as Map)['publicKey'] as String, + ); + } finally { + await dir.delete(recursive: true); + } +} + +void main() { + final endpoint = Platform.environment['CONNECT_TEST_URL']; + test('OpenSSL Ed448 host signing bridge matches shared vector', () async { + final v = vectors().first as Map; + final sig = await nativeEd448(unhex(v['canonicalHex'] as String), v); + expect(sig.signature, (v['proof'] as Map)['signature']); + }); + for (final vector in vectors()) { + final v = vector as Map; + final profile = (v['proof'] as Map)['profile'] as String; + test( + "desktop -> QR -> Dart ${v['name']} -> Better Auth desktop cookie", + () async { + final network = LocalTestNetwork(endpoint!); + final created = await network.call('create', {}); + // Only the URI is passed into the wallet. The test desktop retains the secret. + final wallet = Signer(selection(v['proof'] as Map), ( + bytes, + ) async { + if (profile == 'xcb-ed448') return nativeEd448(bytes, v); + if (profile != 'raw-ed25519') { + final result = await Process.run('node', [ + '../better-connect/scripts/sign-test-message.mjs', + profile, + ((v['proof'] as Map)['account'] as Map)['reference'] as String, + hex(bytes), + ]); + if (result.exitCode != 0) { + throw StateError('Host wallet failed: ${result.stderr}'); + } + final signed = + jsonDecode(result.stdout as String) as Map; + return WalletSignature( + signed['signature'] as String, + publicKey: signed['publicKey'] as String?, + ); + } + final algorithm = Ed25519(), + key = await Ed25519().newKeyPairFromSeed( + unhex(v['testSeed'] as String), + ); + final sig = await algorithm.sign(bytes, keyPair: key); + return WalletSignature( + hex(sig.bytes), + publicKey: hex((await key.extractPublicKey()).bytes), + ); + }); + final client = ConnectClient( + accountProvider: Provider([wallet]), + network: network, + ); + try { + final request = await client.resolve(created['connectUri'] as String); + expect(request.state, ConnectState.awaitingUserApproval); + await request.approve(request.compatibleAccounts.first); + expect(request.state, ConnectState.approved); + final result = await network.call('redeem', { + 'requestId': created['requestId'], + 'redeemSecret': created['redeemSecret'], + }); + expect(result['success'], true); + } finally { + await client.dispose(); + network.close(); + } + }, + skip: endpoint == null + ? 'Run better-connect/scripts/flutter-interop.mjs to start the real server' + : false, + ); + } +}