-
Notifications
You must be signed in to change notification settings - Fork 2
feat(stellar-wallet-snap): add signProofOfOwnership client request RPC
#186
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
base: main
Are you sure you want to change the base?
Changes from all commits
ac01643
bee317a
b734373
3a5b0d2
e28cee5
5a56879
cfa4cc0
3b87a39
04f2d4a
056dee5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| # Use case: `signProofOfOwnership` | ||
|
|
||
| Silently signs a proof-of-ownership message so `@metamask/profile-metrics-controller` can prove the user controls a Stellar address. | ||
|
|
||
| | | | | ||
| | ---------- | --------------------------------------------------------------------------------------------------------------- | | ||
| | **Entry** | `onClientRequest` → `ClientRequestHandler` → `SignProofOfOwnershipHandler` | | ||
| | **Method** | `signProofOfOwnership` (`ClientRequestMethod.SignProofOfOwnership`) | | ||
| | **Source** | [`handlers/clientRequest/signProofOfOwnership.ts`](../../../src/handlers/clientRequest/signProofOfOwnership.ts) | | ||
|
|
||
| This is a **silent sign** — there is no confirmation dialog. That is intentional: the MetaMask client needs an ownership proof without interrupting the user. The method is scoped so it cannot be used as a general sign-message bypass: | ||
|
|
||
| 1. SIP-31 `onClientRequest` is only callable by the MetaMask client. | ||
| 2. The plaintext must be `metamask:proof-of-ownership:{nonce}:{address}`, and the embedded address must match the signing account. | ||
| 3. Signing uses [SEP-0053](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md) (`Wallet.signMessage`). | ||
|
|
||
| ## Request / response (shape) | ||
|
|
||
| **Request params** | ||
|
|
||
| - `accountId` — keyring account UUID | ||
| - `message` — plaintext `metamask:proof-of-ownership:{nonce}:{address}` (see [Message format](#message-format)) | ||
| - `nonce`, `address` — coerced from `message` internally (clients do not send these) | ||
|
|
||
| **Response** | ||
|
|
||
| - `{ signature }` — standard base64 of the 64-byte ed25519 signature (SEP-0053) | ||
|
|
||
| ## Message format | ||
|
|
||
| Parsed by [`parseProofOfOwnershipMessage`](../../../src/handlers/clientRequest/utils.ts) during request validation: | ||
|
|
||
| - Prefix must be exactly `metamask:proof-of-ownership:` (case-sensitive). | ||
| - `{nonce}` is non-empty and may contain `:` characters; parsing splits on the **last** `:` in the remainder. | ||
| - `{address}` must be a valid Stellar strkey (G… public key). | ||
|
|
||
| Example: `metamask:proof-of-ownership:ns:abc:123:GBX…` → nonce `ns:abc:123`, address `GBX…`. | ||
|
|
||
| ## Participants | ||
|
|
||
| | Component | Path | Role in this flow | | ||
| | ----------------------------- | ------------------------ | ---------------------------------------------------- | | ||
| | `ClientRequestHandler` | `handlers/clientRequest` | Routes `signProofOfOwnership` to the handler | | ||
| | `SignProofOfOwnershipHandler` | `handlers/clientRequest` | Validates message, resolves wallet, signs | | ||
| | `AccountResolver` | `handlers/` | Loads keyring account + wallet (no on-chain account) | | ||
| | `AccountService` | `services/account` | Keyring account lookup (via resolver) | | ||
| | `WalletService` / `Wallet` | `services/wallet` | Signing key material + SEP-0053 `signMessage` | | ||
|
|
||
| No confirmation UI or network calls. | ||
|
|
||
| ## Step-by-step | ||
|
|
||
| 1. **Route** — `onClientRequest` dispatches to `SignProofOfOwnershipHandler`. | ||
| 2. **Validate** — Request must match `SignProofOfOwnershipJsonRpcRequestStruct` (prefix, nonce, Stellar address). `nonce` and `address` are coerced from `message`. | ||
| 3. **Resolve** — `AccountResolver.resolveAccount` with `RESOLVE_ACCOUNT_KEYRING_AND_WALLET` loads keyring account and wallet only. The signing account does not need to be activated on-chain. | ||
| 4. **Bind** — The address in the message must equal the signing account address. | ||
| 5. **Sign** — `Wallet.signMessage(message)` (SEP-0053, base64). | ||
|
|
||
| ## Sequence (happy path) | ||
|
|
||
| ```mermaid | ||
| sequenceDiagram | ||
| participant Client | ||
| participant Handler as SignProofOfOwnershipHandler | ||
| participant Resolver as AccountResolver | ||
| participant Wallet | ||
|
|
||
| Client->>Handler: signProofOfOwnership { accountId, message } | ||
| Note over Handler: validate coerces nonce + address from message | ||
| Handler->>Resolver: resolve keyring account + wallet | ||
| Resolver-->>Handler: account, wallet | ||
| Handler->>Handler: message address == account.address | ||
| Handler->>Wallet: signMessage (SEP-0053, base64) | ||
| Wallet-->>Handler: signature | ||
| Handler-->>Client: { signature } | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,11 @@ import { | |
| coerce, | ||
| } from '@metamask/superstruct'; | ||
| import type { JsonRpcRequest } from '@metamask/utils'; | ||
| import { CaipAssetTypeStruct, parseCaipAssetType } from '@metamask/utils'; | ||
| import { | ||
| base64, | ||
| CaipAssetTypeStruct, | ||
| parseCaipAssetType, | ||
| } from '@metamask/utils'; | ||
|
|
||
| import { | ||
| JsonRpcRequestStruct, | ||
|
|
@@ -36,6 +40,7 @@ import { | |
| SwapTransactionXdrStruct, | ||
| } from '../../api'; | ||
| import { isSep41Id } from '../../utils'; | ||
| import { parseProofOfOwnershipMessage } from './utils'; | ||
|
|
||
| /** | ||
| * Enum for the client request method. | ||
|
|
@@ -48,6 +53,11 @@ export const ClientRequestMethod = { | |
| // Standard multichain workflow for bridge | ||
| SignAndSendTransaction: 'signAndSendTransaction', | ||
| ComputeFee: 'computeFee', | ||
| /** | ||
| * Silent proof-of-ownership signing for `@metamask/profile-metrics-controller`. | ||
| * SIP-31 client-only. | ||
| */ | ||
| SignProofOfOwnership: 'signProofOfOwnership', | ||
| /** -------------------------------- Stellar Specific -------------------------------- */ | ||
| ChangeTrustOpt: 'changeTrustOpt', | ||
| } as const; | ||
|
|
@@ -383,6 +393,69 @@ export const ComputeFeeJsonRpcResponseStruct = array( | |
| }), | ||
| ); | ||
|
|
||
| /** | ||
| * Validates that a plaintext message follows the proof-of-ownership format: | ||
| * `'metamask:proof-of-ownership:{nonce}:{address}'`. | ||
| */ | ||
| export const ProofOfOwnershipMessageStruct = refine( | ||
| string(), | ||
| 'ProofOfOwnershipMessage', | ||
| (value: string) => { | ||
| try { | ||
| parseProofOfOwnershipMessage(value); | ||
| return true; | ||
| } catch (error) { | ||
| return error instanceof Error | ||
| ? error.message | ||
| : 'Invalid proof-of-ownership message'; | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| /** | ||
| * Validation struct for the signProofOfOwnership JSON-RPC request. | ||
| * Coerces `nonce` and `address` from `message` (clients send only accountId + message). | ||
| */ | ||
| export const SignProofOfOwnershipJsonRpcRequestStruct = coerce( | ||
| assign( | ||
| JsonRpcRequestStruct, | ||
| object({ | ||
| method: literal(ClientRequestMethod.SignProofOfOwnership), | ||
| params: object({ | ||
| accountId: UuidStruct, | ||
| message: ProofOfOwnershipMessageStruct, | ||
| nonce: nonempty(string()), | ||
| address: StellarAddressStruct, | ||
| }), | ||
| }), | ||
| ), | ||
| assign( | ||
| JsonRpcRequestStruct, | ||
| object({ | ||
| method: literal(ClientRequestMethod.SignProofOfOwnership), | ||
| params: object({ | ||
| accountId: UuidStruct, | ||
| message: ProofOfOwnershipMessageStruct, | ||
| }), | ||
| }), | ||
| ), | ||
| (request) => ({ | ||
| ...request, | ||
| params: { | ||
| ...request.params, | ||
| ...parseProofOfOwnershipMessage(request.params.message), | ||
| }, | ||
| }), | ||
| ); | ||
|
|
||
| /** | ||
| * Validation struct for the signProofOfOwnership JSON-RPC response. | ||
| * Standard base64 of the 64-byte ed25519 signature (SEP-0053). | ||
| */ | ||
| export const SignProofOfOwnershipJsonRpcResponseStruct = object({ | ||
| signature: nonempty(base64(string())), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think the Auth API expects base64 here. Can you verify with the auth team (most probably @dovydas55) that they expect 0x-prefixed hex for Stellar as well? SEP-0053 is the right signing scheme, this is only about encoding the 64-byte sig on the wire. We had the same issue with Solana base58 and had to transcode to 0x hex: #617 If they confirm hex, this return and |
||
| }); | ||
|
|
||
| /** | ||
| * A JSON-RPC request with an account resolve parameter. | ||
| */ | ||
|
|
@@ -474,3 +547,17 @@ export type ComputeFeeJsonRpcRequest = Infer< | |
| export type ComputeFeeJsonRpcResponse = Infer< | ||
| typeof ComputeFeeJsonRpcResponseStruct | ||
| >; | ||
|
|
||
| /** | ||
| * Type for the signProofOfOwnership JSON-RPC request. | ||
| */ | ||
| export type SignProofOfOwnershipJsonRpcRequest = Infer< | ||
| typeof SignProofOfOwnershipJsonRpcRequestStruct | ||
| >; | ||
|
|
||
| /** | ||
| * Type for the signProofOfOwnership JSON-RPC response. | ||
| */ | ||
| export type SignProofOfOwnershipJsonRpcResponse = Infer< | ||
| typeof SignProofOfOwnershipJsonRpcResponseStruct | ||
| >; | ||
Uh oh!
There was an error while loading. Please reload this page.