Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 0.1.3

- Add optional chain-aware wallet address validation and shared checksum/network fixtures.

- Add interoperable multi-chain Wallet Operations models, capabilities, adapters, explicit approval, dispatch, safe errors and encrypted transport codecs.
- Add shared protocol/encryption fixtures, regression coverage and host integration examples.
- Retain existing authentication protocol and APIs.

## 0.1.2

- Align publishing with the Flutter reference libraries: validate on tag push, then publish through Dart's reusable pub.dev workflow.
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,15 @@ Shared vectors assert identical Dart/TypeScript messages. Dart Ed25519 signature
See [release setup](doc/RELEASE.md). Licensed under the [CORE License](LICENSE).

This package is distributed under the [CORE License](LICENSE). It is not an OSI-approved license.

# Wallet Operations

Connect transports signing requests; the host wallet owns and uses the private key.

`ConnectOperation`, `OperationResponse`, `OperationCapability` and `OperationDispatcher` implement the canonical connect-protocol operation format. Chain adapters validate native payloads and declare capabilities. Wallet/custom operations require explicit host approval; read-only chain operations use separate handlers. Existing account providers supply identities without exposing keys or invoking authentication signers.

See the [operation protocol and API guide](doc/OPERATIONS.md) and [EVM, Bitcoin PSBT, Solana, Core and balance examples](example/lib/operations.dart). `OperationTransport` handles host messaging, and `OperationCipher` optionally encrypts requests/responses. Existing authentication, links and handoffs remain unchanged.

## Wallet address validation

`validateWalletAccount` checks addresses against an explicit Connect namespace/reference and returns `valid`, `invalid`, or `unsupported`. `isValidWalletAccount` accepts only `valid`. These checks supplement wallet ownership verification. See [coverage, adapter integration and package compatibility](doc/WALLET_VALIDATION.md).
131 changes: 131 additions & 0 deletions doc/OPERATIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Wallet Operations

Connect transports signing requests; the host wallet owns and uses the private key.

`connect-protocol` defines the canonical wire format; `flutter_connect` implements the same format. Authentication remains a separate, unchanged Connect v1 exchange. The operation APIs are additive in package version 0.1.3.

```text
DApp
|
v
Connect Protocol
|
+---- Wallet Operations ----> Wallet ----> Signer
|
+---- Chain Operations -----> RPC/Node ----> Blockchain
|
+---- Capability Negotiation
|
+---- Chain Adapter
```

## Wire format

Operations reuse Connect's 64-character lowercase hexadecimal `requestId` and `{namespace, reference, address}` account identity. An account, when present, must match the envelope chain. `createOperation` (TypeScript) and `ConnectOperation` (Dart) generate cryptographically random IDs unless supplied by the caller.

```json
{
"version": 1,
"requestId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"operation": "wallet.signTransaction",
"chain": { "namespace": "eip155", "reference": "1" },
"account": { "namespace": "eip155", "reference": "1", "address": "0x1111111111111111111111111111111111111111" },
"params": { "transaction": { "to": "0x2222222222222222222222222222222222222222", "value": "0x1" } },
"metadata": { "application": "Example dapp" }
}
```

Success:

```json
{"version":1,"requestId":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","result":{"signedTransaction":"0x..."}}
```

Rejection:

```json
{"version":1,"requestId":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","error":{"code":"USER_REJECTED","message":"User rejected the operation."}}
```

`result` can be any JSON value, including null. Exactly one of `result` and `error` is required. Error `details` is an optional JSON object. Dispatchers emit fixed safe messages and omit details; transport parsers accept safe host-defined details. A malformed request without a valid ID receives `requestId: null`; all other responses retain the supplied valid ID. Unknown envelope versions yield `INVALID_PARAMS`. `parseOperationResponse` / `OperationResponse.fromJson` can enforce an expected ID; `requestOperation` always enforces it.

Requests, responses and capability lists are limited to 16,384 UTF-8 bytes and nesting depth 32. Unknown envelope fields, non-JSON values, unsafe integer numbers, and common secret field names (`privateKey`, `secretKey`, `mnemonic`, `seedPhrase`, including underscore/hyphen variants) are rejected. Use decimal or hex **strings** for balances, fees and large integers; represent bytes explicitly as base64/hex strings. These checks do not detect arbitrary secrets inside opaque strings: host handlers must return only public results. Never put keys, seeds, PINs or credentials into payloads or metadata.

## Operations and capabilities

| Methods | Behavior |
| --- | --- |
| `wallet.getAccounts` | Explicitly approved identity disclosure; `operationAccounts` reuses existing account providers without invoking their authentication signers. |
| `wallet.signMessage` | Host message signing; adapter may prepare a message for the approval callback. |
| `wallet.signTransaction`, `wallet.sendTransaction` | Native chain-specific transaction payload; host signs or signs/broadcasts. |
| `wallet.switchChain`, `wallet.addChain` | Host wallet network policy/configuration. The envelope identifies the requested target chain. |
| `wallet.signTypedData`, `wallet.signPsbt`, `wallet.signAllTransactions` | Optional specialized capabilities. Never inferred from namespace. |
| `chain.getBalance`, `chain.getTransaction`, `chain.getBlock`, `chain.estimateFee`, `chain.call` | Read-only host RPC/node handlers, without wallet approval or signer access. |

`OperationDispatcher.capabilities` is an immutable advertisement suitable for the host's authenticated session setup. Serialize it as a JSON array; Dart capabilities expose `toJson()`. `parseCapabilities` validates advertisements and rejects duplicate chain entries or methods. `supportsOperation` and `dispatcher.supports` query support by both namespace and reference.

```json
[
{"namespace":"eip155","reference":"1","methods":["wallet.signTransaction","chain.getBalance"]},
{"namespace":"bip122","reference":"000000000019d6689c085ae165831e93","methods":["wallet.signPsbt"]}
]
```

Only capabilities with registered handlers can be advertised. Different networks can expose different methods. Capability advertisement is not authorization: the wallet still approves each wallet/custom request. There is no implicit chain switching or automatic RPC configuration.

## Chain adapters

`OperationChainAdapter` declares capabilities, handlers, `validateAccount` and `validateOperation`. Validation must check all chain-specific transaction/message fields, network bindings and encodings before approval. An invalid address returns false; validation can throw `OperationError('INVALID_TRANSACTION')` / `OperationException('INVALID_TRANSACTION')`. Optional hooks provide message preparation, result normalization and safe error-code translation. Preparation must not sign or obtain keys. Handlers receive the same immutable validated request that approval receives. The prepared message is additional approval context; a signing handler can use the same deterministic preparation routine.

Use existing `chains` / `ConnectChains` presets for namespace/reference values. TypeScript chain definitions structurally satisfy `OperationChain`; Dart uses `OperationChain.fromChain`. Register separate capabilities for distinct references; duplicate adapter registrations for the same chain are rejected.

The extension interface supports all existing chain presets: Core, Ethereum, Base, Polygon, BNB Smart Chain, Bitcoin, Litecoin, Solana, TRON, XRP Ledger, Stellar, Cardano, Monero and Zcash. It does **not** provide native transaction parsers, signers, broadcasters or RPC clients for these chains. Those belong to the host's SDKs. An authentication signing profile does not imply any transaction capability. For example, advertise Bitcoin PSBT only if the wallet actually supports it; Litecoin/Zcash do not acquire PSBT support from sharing `bip122`.

There is no universal transaction schema. Optional TypeScript `EvmTransactionParams`, `PsbtParams`, `SolanaTransactionParams` and `CoreTransactionParams` describe outer native-payload containers only, not full SDK validation. A Bitcoin PSBT remains a PSBT; a Solana serialized transaction remains Solana data; Core uses its own transaction fields such as energy.

## Approval, accounts and dispatch

Create a dispatcher per authenticated host session with adapters, an existing `AccountProvider` / `WalletAccountProvider`, and an explicit `approve` callback. Dispatch with an origin obtained from the trusted host transport. HTTPS origin syntax is validated but does not authenticate the sender. Request `metadata` is an untrusted application claim; never derive the trusted origin from it.

The approval callback receives the full chain, account, params, metadata, trusted origin and cancellation context. Decode amount, destination, fee and network with the host chain SDK and show them with the requesting application before approval. The protocol contains no UI. All `wallet.*` and custom methods require approval; omission returns `UNAUTHORIZED`, false returns `USER_REJECTED`. Read-only `chain.*` handlers bypass approval. Do not register a signing implementation under a read-only chain method.

Signing/sending methods require an account, a successful adapter address check and membership in the existing host account provider. Account availability is checked again after approval. Authentication account `sign()` callbacks are never reused to sign arbitrary transactions. Existing account identity/profile representations remain unchanged.

Dispatch flow: parse/snapshot → bind origin and consume ID → resolve chain → check capability → check account → validate payload → prepare/approve wallet request → recheck account → execute host handler → normalize public result → validate/serialize correlated response.

A session consumes each valid ID once, including rejected/failed requests. It holds at most 4,096 IDs by default and refuses further requests rather than evicting replay protection. Host sessions must manage authorization lifetime and persist replay protection if requests must survive process restarts. Create new IDs for genuinely new operations; don't blindly retry a send after a timeout.

Default execution timeout is 30 seconds, configurable through `timeoutMs` / `timeout`. TypeScript handlers receive an `AbortSignal`; Dart handlers can check `context.isCancelled`. Late approval cannot start a handler after timeout. A host operation already in progress cannot be rolled back by Connect; the host must honor cancellation where possible and reconcile uncertain broadcasts. Creating a new dispatcher resets in-memory replay state.

## Custom operations

Register `myapp.customOperation` in both an adapter's methods and handlers. Names match `[a-z][a-z0-9]*\.[a-zA-Z][a-zA-Z0-9]*` and are at most 128 characters. `wallet.*` and `chain.*` are reserved: only the listed standard methods can be registered there. Custom operations require explicit approval and adapter validation. Unregistered names return `UNSUPPORTED_OPERATION`.

## Transport and encryption

`OperationTransport.exchange` sends and receives JSON strings; `requestOperation` validates and correlates responses. A host can implement HTTPS, extension messaging, QR/manual return, Bluetooth or any future message channel. `dispatcher.dispatchJson` is the wallet endpoint. The protocol performs no network I/O or UI by itself.

`OperationCipher(origin, requestId, pairingKey)` optionally wraps requests and responses using the existing Connect AES-256-GCM convention: 32-byte pairing key, fresh random 12-byte nonce, ciphertext followed by 16-byte authentication tag, unpadded base64url. Keys are supplied through a trusted pairing channel and are never wallet signing keys. The API keeps them private internally. Request and response directions use distinct authenticated data:

```text
connect:operation:1/{request|response}/{https-origin}/{requestId}
```

Encrypted packets have exactly `type: "connect:operation:encrypted:1"`, `kind`, `requestId`, `iv`, `ciphertext`; maximum packet size is 32,768 characters. This new type cannot be confused with authentication's `connect:response:1`. `sealRequest`/`openRequest` and `sealResponse`/`openResponse` enforce direction and correlation. Existing Dart `ConnectBleResponseBuffer` can frame these strings without change. QR and deep-link hosts carry the same packet using their own routing and trusted pairing setup; existing authentication links are not operation links. Encryption provides channel confidentiality/integrity, not wallet ownership verification. Do not log pairing material.

## Errors

Stable codes: `UNSUPPORTED_OPERATION`, `UNSUPPORTED_CHAIN`, `UNSUPPORTED_ACCOUNT`, `INVALID_PARAMS`, `INVALID_TRANSACTION`, `USER_REJECTED`, `UNAUTHORIZED`, `SIGNING_FAILED`, `BROADCAST_FAILED`, `CHAIN_UNAVAILABLE`, `TIMEOUT`, `INTERNAL_ERROR`.

Throw a protocol exception with a code, or use the adapter's `translateError` callback. Unrecognized callback exceptions are replaced with a fixed stage-appropriate message; exception contents never enter protocol responses. Approval exceptions become `UNAUTHORIZED`; signing errors become `SIGNING_FAILED`; send errors become `BROADCAST_FAILED`; read-only handler errors become `CHAIN_UNAVAILABLE`.

## Examples and compatibility

The operation examples demonstrate EVM signing, Bitcoin PSBT signing, Solana serialized transaction signing, Core transaction signing and read-only balances through explicit host callbacks. They require real SDK validation and host signing; fixture transactions are serialization examples, not broadcastable transactions.

- TypeScript/JavaScript: `examples/operations.mjs`, exported `createExampleOperations` and `exampleRequests`.
- Flutter/Dart: `example/lib/operations.dart`, `OperationsHost`, `createExampleOperations` and `exampleRequests`.

Shared `test/fixtures/operations/conformance.json` covers chain identities, all methods, custom methods, responses, capabilities and deterministic encrypted interoperability. Both implementations parse the same fixtures. Live cross-language tests also exchange freshly encrypted packets. Existing authentication and handoff tests remain regression coverage; no authentication messages, URI formats, signers, or verification profiles were changed.

Run `npm run test:operations-interop` from connect.js with flutter_connect alongside it (or run `node scripts/test-operation-interop.mjs /path/to/flutter_connect` after the build). The command compares shared fixtures and verifies freshly encrypted requests and responses in both languages. Normal validation remains `npm test`, `npm run typecheck`, `npm run lint`, `npm run test:browser`, `npm run test:package`, `flutter analyze`, and `flutter test` in both the Flutter library and example.
47 changes: 47 additions & 0 deletions doc/WALLET_VALIDATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Wallet address validation

`validateWalletAccount({namespace, reference, address})` returns a safe `status`: `valid`, `invalid`, or `unsupported`. `isValidWalletAccount` returns true only for `valid`. Dart uses the existing `WalletIdentity` and `WalletValidationStatus` enum. The helpers never rewrite the account identity or expose upstream exception messages.

The integrations pin `blockchain-wallet-validator` 1.2.1 and `flutter_wallet_validator` 0.1.4. Explicit namespace/reference mapping prevents automatic network detection or ENS/domain acceptance. Address validity does not prove ownership, account availability, selected network, or transaction safety. Existing signature verification and host approval remain required.

## Operation adapters

Wrap a host adapter with `withWalletValidation(adapter)` in either library. A known-invalid address is rejected before host approval or handler execution. The existing host account validator still runs for valid addresses and for unsupported chains/formats; it remains responsible for handling those extensions. The operation examples enable this wrapper.

```ts
const dispatcher = new OperationDispatcher({
adapters: [withWalletValidation(myChainAdapter)],
accounts: walletAccounts,
approve: showApproval,
});
```

For transaction destinations, call `validateWalletAccount` with the request's chain and the decoded destination address from inside `validateOperation`. Reject `invalid`; use the chain SDK for `unsupported`. Connect does not guess transaction field names or impose a universal transaction format. Resolve name-service identifiers separately before creating an account identity.

## Coverage and limitations

| Namespace/reference | Address checks |
| --- | --- |
| `core:1`, `core:3` | ICAN checksum and exact mainnet/Devin prefix, case-insensitive validation without rewriting identity |
| `eip155:<positive decimal ID>` | 20-byte hex format; mixed-case EIP-55 checksum; unchecksummed lowercase/uppercase bodies accepted |
| Bitcoin/Litecoin existing mainnet `bip122` presets | Base58Check version/length; Bech32/Bech32m witness version, program length, checksum and mainnet HRP |
| Existing Solana mainnet preset | Base58 decoding to exactly 32 bytes, without unrelated-chain prefix heuristics |
| Existing TRON and XRP presets | Address version, decoded length and checksum |
| `stellar:pubnet` | Public-account StrKey version and CRC checksum |
| Existing Cardano mainnet preset | Shelley base, enterprise and reward addresses; Bech32 checksum, header, length and network tag |

EVM addresses do not encode a chain ID. Solana, XRP and other shared address formats likewise cannot prove which network the wallet selected. Chain capabilities, session policy and the signer must enforce that binding.

Monero, Zcash, unknown chain references and Cardano pointer forms require existing host/profile validation. They are never reported as validated by this helper. Byron and other unrecognized address forms are not accepted by the mapped Cardano preset; applications needing them must supply a dedicated adapter without this wrapper. No additional testnets are inferred from an address or a boolean testnet option.

## Package compatibility checks

The TypeScript package's generic result is insufficient for every supported format: legacy Base58Check addresses, SegWit checksums and Cardano enterprise addresses need supplemental codec checks. The Flutter package also has restrictive Cardano patterns and Solana prefix heuristics. The wrappers use the packages for available checks and supplement those gaps with explicit decoding. They never turn a failed checksum into an accepted address based on a regex.

`test/fixtures/wallet-validation.json` is identical across connect.js, flutter_connect and better-connect. Its 55 cases cover existing account fixtures, corrupted checksums, wrong networks, EVM casing, Core Devin, Taproot, Cardano headers, unsupported chains and valid Solana keys beginning with `r` or `T`. Each repository runs these cases locally; operation/server tests additionally verify rejection before approval or proof verification.

## better-connect

The server applies these checks automatically in `ConnectEngine.approve`, after envelope/profile compatibility checks and before profile verification. Known-invalid accounts return `unsupportedAccount` and leave the request pending. Unsupported chains continue through the configured signing profile's validator and cryptographic proof verification. A valid address never bypasses proof verification.

better-connect exports the same `validateWalletAccount` and `isValidWalletAccount` helpers. Its adapter source and fixtures mirror connect.js while its existing immutable connect-protocol SDK pin remains unchanged; it does not depend on unpublished workspace code.
Loading
Loading