-
Notifications
You must be signed in to change notification settings - Fork 2
Enhance InputRequest and permission model in docs #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
iamalwaysuncomfortable
wants to merge
6
commits into
master
Choose a base branch
from
design/wallet-specified-inputs
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
0dc4fb9
Enhance InputRequest and permission model in docs
iamalwaysuncomfortable be4f23e
Add readAddress field to ConnectHistory interface
iamalwaysuncomfortable c6ad425
Refine InputRequest discriminator and permission model
iamalwaysuncomfortable 5067daf
Specify readAddress as a permission
iamalwaysuncomfortable fa653fe
Implement wallet-specified inputs and structured permission grants
iamalwaysuncomfortable 63f5ad8
Merge pull request #85 from ProvableHQ/feat/wallet-specified-inputs
iamalwaysuncomfortable File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| --- | ||
| '@provablehq/aleo-types': minor | ||
| '@provablehq/aleo-wallet-standard': minor | ||
| '@provablehq/aleo-wallet-adaptor-core': minor | ||
| '@provablehq/aleo-wallet-adaptor-leo': minor | ||
| '@provablehq/aleo-wallet-adaptor-fox': minor | ||
| '@provablehq/aleo-wallet-adaptor-soter': minor | ||
| '@provablehq/aleo-wallet-adaptor-puzzle': minor | ||
| '@provablehq/aleo-wallet-adaptor-shield': minor | ||
| '@provablehq/aleo-wallet-adaptor-react': minor | ||
| --- | ||
|
|
||
| Add wallet-specified input requests and structured permission grants | ||
|
|
||
| `TransactionOptions.inputs` is now `TransactionInput[]` (= `(string | InputRequest)[]`). Dapps can place an `InputRequest` in any slot to ask the wallet to fill in the active address, derive a value from the user's view key, or auto-select an owned record matching dapp-supplied filters. Passing literal `string[]` continues to work — `string` is a subtype of `TransactionInput`. | ||
|
|
||
| Adapters that do not yet implement fulfillment (leo, fox, soter, puzzle) throw `WalletInputRequestNotSupportedError` when an `InputRequest` is encountered. Shield forwards inputs to the extension, which is expected to support them. | ||
|
|
||
| `connect()` accepts a new optional `options?: ConnectOptions` parameter carrying `recordAccess`, `viewKeyExposure`, and `readAddress`. When `readAddress: false`, the toolkit short-circuits `decrypt`, `requestRecords`, `transitionViewKeys`, and `requestTransactionHistory` with `WalletAddressWithheldError`. Connections with `readAddress: false` are only valid alongside `decryptPermission: NoDecrypt`. Adapters other than shield throw `WalletConnectOptionsNotSupportedError` when these options are set. | ||
|
|
||
| The `<AleoWalletProvider>` React component accepts new props `recordAccess`, `viewKeyExposure`, and `readAddress` and forwards them on connect. Existing usages without these props are unaffected. | ||
|
|
||
| If your code reads `TransactionOptions.inputs[i]` as a string, narrow with `typeof i === 'string'` (or use the exported `isLiteralInput` type guard) before passing it to a `string`-typed API. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| # Dapp input requests for `executeTransaction` | ||
|
|
||
| ## Goal | ||
|
|
||
| Let dapps emit `TransactionOptions` whose `inputs` slots are not always literal Aleo values. Each non-literal slot is a **request** to the wallet — to prompt the user, to auto-select an owned record matching dapp-supplied criteria, or to derive a value from the user's view key. The wallet fulfills the request before passing the transaction to the SDK. | ||
|
|
||
| ## Wire-level types | ||
|
|
||
| ```ts | ||
| type Input = string | InputRequest; | ||
|
|
||
| type InputRequest = | ||
| | { type: "address"; label?: string } // Specification to fill the input field with the active address. Allowed in an input position with an aleo type of: `address, group, scalar, or field`. | ||
| | { type: "record"; program: string; filters?: RecordFilters } // Specification to use a record from a specific program with given filters. Allowed in an input position with an aleo type of: `record, dynamic_record, or external_record`. | ||
| | { type: "viewKey"; label?: string }; // Specification to fill the input field with the view key behind the active address. Allowed in a input position with an aleo type of: `scalar or field`. | ||
|
marshacb marked this conversation as resolved.
|
||
|
|
||
| type RecordFilters = Record<string, RecordFieldFilter>; // keys are top-level record field names or dotted paths into struct fields, e.g. "amount" or "data.amount". | ||
| type RecordFieldFilter = { eq?: string, gte?: string, lte?: string, neq?: string, }; // potential matching conditions, AND-combined. | ||
| ``` | ||
|
|
||
| The `InputRequest` sends a request to the wallet (which is then authorized by the user) to do the following: | ||
| 1. Input the user's address into a position where there's an address, group, scalar, or field input. | ||
| 2. Input a view key if where there's a field or scalar input. | ||
| 3. Use a record whose fields match the `filters` on specific record's members and filter for records that match them if applicable, returning an error if the condition cannot be applied or a record matching it cannot be found. | ||
|
|
||
| The wallet has the program's source, so it reads a function's parameter signature for input position `i` and renders the form control accordingly. `label` is UX-only. | ||
|
|
||
| Adapters are ONLY allowed to successfully execute this if the user has authorized permission to do so. | ||
|
|
||
| ## Permission model | ||
|
|
||
| ### Today | ||
|
|
||
| `ConnectHistory` (`src/app/common/types/IAdapterService.ts:92`) carries `decryptPermission` plus a flat `programs?: string[]` allowlist. Two gates consult `programs` via `programs.includes(target)`: `executeTransaction` at `AdapterService.ts:240` and `requestRecords` at `:494`. Permissions are scoped per-dapp via `siteInfo.origin`; every gated method runs `getMatchingConnectHistoryAndDecryptPermission` (`:412-437`) first, which loads exactly one `ConnectHistory` row keyed on `(origin, network, address)`. | ||
|
|
||
| ### Proposed | ||
|
|
||
| Add three new fields to `ConnectHistory`, all additive. The existing `decryptPermission` and `programs?: string[]` are preserved exactly, and the `connect()` signature does not change. | ||
|
|
||
| ```ts | ||
| interface ConnectHistory { | ||
| // ...existing fields... | ||
| decryptPermission: DecryptPermission; // unchanged | ||
| programs?: string[]; // unchanged — program-level gate for both transaction execution and record operations | ||
| readAddress?: boolean; // new — opt-in address withholding; default true | ||
| recordAccess?: RecordAccessGrant; // new — opt-in record/field narrowing | ||
| viewKeyExposure?: "DENY" | "PER_TX_PROMPT"; // new — default DENY | ||
| } | ||
|
|
||
| type RecordAccessGrant = | ||
| | { level: "none" } | ||
| | { level: "byProgram"; programs: ProgramGrant[] }; | ||
|
|
||
| interface ProgramGrant { | ||
| program: string; | ||
| records?: RecordGrant[]; // undefined → all records of this program; present → only the listed records | ||
| } | ||
|
|
||
| interface RecordGrant { | ||
| recordname: string; | ||
| fields?: FieldGrant[]; // undefined → all fields; present → only the listed fields | ||
| } | ||
|
|
||
| interface FieldGrant { | ||
| name: string; | ||
| readAccess?: boolean; // undefined → true; false → field is usable as a filter key but plaintext is withheld on decrypt | ||
| } | ||
| ``` | ||
|
|
||
| | Configuration | Meaning | | ||
| |---|---| | ||
| | `recordAccess: undefined` | Today's broad behavior — all records of programs allowed by `programs`, all fields. | | ||
| | `recordAccess: { level: "none" }` | Refuse every `requestRecords` call and every `type: "record"` request. Transaction execution with literal inputs is unaffected. | | ||
| | `recordAccess: { level: "byProgram", programs: [...] }`, `ProgramGrant.records` undefined | All records of the listed program; all fields. | | ||
| | `recordAccess: { level: "byProgram", programs: [...] }`, `RecordGrant.fields` undefined | Only the listed records; all fields within them. | | ||
| | `recordAccess: { level: "byProgram", programs: [...] }`, `fields` listed | Only the listed records, and only the listed fields within each. | | ||
|
|
||
| ### Backward compatibility | ||
|
|
||
| The pre-existing dapp surface is preserved exactly: | ||
|
|
||
| - **`connect()` signature unchanged**: still `(siteInfo, network, decryptPermission, programs?)`. A dapp that called `connect({ programs: ["foo.aleo"] })` before this change behaves identically after. | ||
| - **Existing gates unchanged**: the `programs.includes(program)` checks at `AdapterService.ts:240` and `:494` keep producing the same outcome for any connection where `recordAccess` is undefined. | ||
| - **`recordAccess` defaults to undefined**: the wallet never synthesizes a grant from the legacy `programs` list. `undefined` reads as "today's broad behavior." | ||
| - **`viewKeyExposure` defaults to `DENY`**: matches today's de-facto behavior, since no view-key-derived inputs were possible. | ||
| - **Per-dapp scoping**: `recordAccess` lives on the `ConnectHistory` row keyed on `(origin, network, address)`; one dapp's grant never affects another's access. No change to today's scoping. | ||
|
|
||
| A strict opt-in security model would require an explicit grant for any record access. Keeping the default broad here is a deliberate trade-off to avoid breaking dapps that connected before `recordAccess` existed. Dapps that want narrower scopes opt in by populating `recordAccess` at connect time. | ||
|
|
||
| ### Interaction rules | ||
|
|
||
| When `recordAccess` is set, these rules apply on top of the unchanged `programs` allowlist: | ||
|
|
||
| 1. **Subset constraint**: every `recordAccess.programs[].program` must appear in `programs`. Connect-time validation rejects mismatches. | ||
| 2. **Programs without record grants lose record access**: a program in `programs` but not in `recordAccess.programs[]` keeps transaction-execution access (literal inputs only). It cannot be queried via `requestRecords` and cannot be the target of a `type: "record"` request. | ||
| 3. **Record narrowing**: when `ProgramGrant.records` is present, only the listed `recordname`s of that program are accessible. `undefined` → all records. | ||
| 4. **Field narrowing**: when `RecordGrant.fields` is present, only the listed field names may be referenced as filter keys in a `type: "record"` request. `undefined` → all fields. Filter keys outside the listed fields are a permission error at the gate. Plaintext exposure is further controlled by `FieldGrant.readAccess`: when `readAccess` is `true` (or omitted, which defaults to `true`) the field's plaintext value is included in `requestRecords` decrypt output; when `false` the field remains usable as a filter key but its plaintext is redacted from decrypt results. | ||
| 5. **`level: "none"`** refuses all record operations regardless of `programs`. Transaction execution with literal inputs is unaffected. | ||
|
|
||
| ### Address exposure | ||
|
|
||
| `readAddress?: boolean` controls whether the dapp learns the user's address. Defaults to `true` (undefined treated as `true`); `false` is opt-in for privacy-preserving dapps. | ||
|
|
||
| #### `readAddress: undefined | true` — current behavior, with notification | ||
|
|
||
| The dapp learns the address through the same paths as today. The only change is **UX**: the connect dialog adds an explicit line item disclosing that the dapp will see the address. The user already approves the connection itself; this surfaces what the approval implies. No API or return-type change. | ||
|
|
||
| #### `readAddress: false` — withholding | ||
|
|
||
| The dapp transacts on the user's behalf without learning the address. Every direct-exposure path is closed and every operation that would let the dapp enumerate, decrypt, or otherwise derive the address from wallet-mediated state is refused. | ||
|
|
||
| | Surface | Behavior under `readAddress: false` | | ||
| |---|---| | ||
| | `connect()` return value (`Account.address`) | `""` (empty string) — type stays `string` | | ||
| | `provider._publicKey` getter | returns `""` regardless of connection state | | ||
| | `init` message handler | must return `address: ""` if ever extended to populate; today's `undefined` already complies | | ||
| | `decrypt(record)` | refused with permission error | | ||
| | `requestRecords(program, includePlaintext?)` | refused with permission error — use `type: "record"` requests for transaction inputs instead | | ||
| | `transitionViewKeys(txid)` | refused with permission error (view keys derive the address via `address = view_key · G`) | | ||
| | `requestTransactionHistory(program)` | refused with permission error (same view-key-derivation reasoning) | | ||
| | `executeTransaction` with literal inputs | allowed | | ||
| | `executeTransaction` with `type: "address"` slot | allowed — the wallet injects the active address into the transaction; the dapp never observes it | | ||
| | `signMessage(message)` | allowed — the signature plus message reveals the signer's public key, which is the address. The privacy guarantee leaks here by design; dapps that need a strict guarantee should not call `signMessage` under `readAddress: false`. | | ||
|
|
||
| #### Compatibility constraint with `decryptPermission` | ||
|
|
||
| Because every plaintext-bearing decrypt operation is refused under `readAddress: false`, the only coherent `decryptPermission` value is `NoDecrypt`. Connecting with `readAddress: false` together with `UponRequest`, `AutoDecrypt`, or `OnChainHistory` is a connect-time error. | ||
|
|
||
| #### Backward compatibility | ||
|
|
||
| The default (`undefined` or `true`) preserves today's behavior verbatim at every API surface. The connect-dialog notification is a UI-only change visible to the user, not the dapp. No existing dapp's wire calls or return values change. | ||
|
|
||
| ### Independent rule for `type: "user"` | ||
|
|
||
| A wallet only renders a user-input prompt for parameters declared `.private` in the program source. Public parameters must be supplied as literals by the dapp. Enforced statically in the resolver, not in `ConnectHistory`. | ||
|
|
||
| ## Fulfillment flow | ||
|
|
||
| ```mermaid | ||
| flowchart TD | ||
| A["dapp: executeTransaction<br/>inputs: Input[]"] --> B["validation.ts<br/>schema accepts string | InputRequest"] | ||
| B --> C{"AdapterService<br/>permission gate"} | ||
| C -- "violates recordAccess<br/>or viewKeyExposure" --> X["error to dapp"] | ||
| C -- ok --> D["ExecuteTransaction page"] | ||
| D --> R["fulfillInputRequests.ts"] | ||
| R -- "type: record" --> R1["filter unspent records<br/>by where clause"] | ||
| R -- "type: viewKey" --> R2["derive value via SDK"] | ||
| R -- "type: user" --> R3["render typed form<br/>from program signature"] | ||
| R1 --> F["confirm screen<br/>shows every fulfilled value"] | ||
| R2 --> F | ||
| R3 --> F | ||
| F -- user confirms --> G["initializeGenericTransaction<br/>(fulfilled string[], lockedRecords)"] | ||
| G ===> H["worker.ts → SDK<br/>UNCHANGED"] | ||
|
|
||
| classDef boundary stroke-dasharray: 5 5; | ||
| class H boundary; | ||
| ``` | ||
|
|
||
| The worker boundary still receives `string[]`. All fulfillment is wallet-side; the SDK call sites and the `imports` path are untouched. | ||
|
|
||
| ## Failure modes | ||
|
|
||
| | Request | Condition | Result | | ||
| |---|---|---| | ||
| | `type: "record"` | zero matches for `where` | fail loudly (matches `imports` precedent) | | ||
| | `type: "record"` | filter key does not resolve to a field in the record's signature (including dotted struct paths) | validation error before gate | | ||
| | `type: "record"` | operator illegal for the field's Aleo type (e.g. `gte` on `boolean`) | validation error before gate | | ||
| | `type: "record"` | filter value does not parse as a literal of the field's Aleo type | validation error before gate | | ||
| | `type: "record"` | `gte` and `lte` form an empty range, or `eq` contradicts `neq` | validation error before gate | | ||
| | `type: "record"` | field outside `ProgramGrant.fields` | permission error at gate | | ||
| | `type: "user"` | parameter declared `.public` | fulfillment error before prompting | | ||
| | `type: "viewKey"` | `viewKeyExposure: "DENY"` | permission error at gate | | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.