Skip to content

Commit ad937d3

Browse files
feat: init OpenRPC doc with client_signAndSendTransactionWithIntent
1 parent 184a914 commit ad937d3

4 files changed

Lines changed: 240 additions & 47 deletions

File tree

packages/snap/openrpc.json

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
{
2+
"openrpc": "1.0.0",
3+
"info": {
4+
"title": "MetaMask Solana Wallet Snap JSON-RPC API",
5+
"description": "JSON-RPC API of the MetaMask Solana Wallet Snap. This snap brings official Solana support to MetaMask. Create accounts, check balances, and use Solana dApps right from your MetaMask wallet. Simple, secure, and seamless.",
6+
"version": "1.0.0",
7+
"contact": {
8+
"name": "New Networks Team"
9+
},
10+
"license": {
11+
"name": "MIT"
12+
}
13+
},
14+
"servers": [
15+
{
16+
"name": "MetaMask Solana Wallet Snap",
17+
"url": "snap://npm:@metamask/solana-wallet-snap"
18+
}
19+
],
20+
"methods": [
21+
{
22+
"name": "client_signAndSendTransactionWithIntent",
23+
"summary": "Sign and send a transaction with verified intent",
24+
"description": "Allows the client (MetaMask) to request the snap to sign and send a transaction with verified intent.\n\n**Process:**\n\n1. **Verify Backend Signature** - Ensures the payload `{ intent, signature }` was signed by the trusted Swaps API\n2. **Match Transaction to Intent** - Verifies the transaction performs exactly what the intent describes\n3. **Sign Transaction** - If all validation pass, signs the transaction with the user's account specified in the intent\n4. **Send Transaction** - Sends the transaction to the Solana network",
25+
"params": [
26+
{
27+
"name": "intent",
28+
"summary": "The user's intent describing the desired swap/bridge operation",
29+
"required": true,
30+
"schema": {
31+
"$ref": "#/components/schemas/Intent"
32+
}
33+
},
34+
{
35+
"name": "transaction",
36+
"summary": "The base64-encoded transaction to sign and send",
37+
"required": true,
38+
"schema": {
39+
"type": "string"
40+
}
41+
},
42+
{
43+
"name": "signature",
44+
"summary": "The result of signing the payload `{intent, transaction}` using <TODO SIGNING ALGORITHM> with the Swaps API's private key.",
45+
"required": true,
46+
"schema": {
47+
"type": "string"
48+
}
49+
}
50+
],
51+
"result": {
52+
"name": "SignAndSendTransactionWithIntentResult",
53+
"summary": "The result of signing and sending a transaction with intent",
54+
"schema": {
55+
"$ref": "#/components/schemas/SignAndSendTransactionWithIntentResult"
56+
}
57+
},
58+
"examples": [
59+
{
60+
"name": "signAndSendTransactionWithIntentExample",
61+
"description": "Example of signing and sending a transaction with intent, where user wants to swap 1 SOL for 161.18 USDC.",
62+
"params": [
63+
{
64+
"name": "intent",
65+
"value": {
66+
"id": "c76b3834-b9a8-4dfd-91da-72c26a216fb2",
67+
"timestamp": "2021-01-01T00:00:00Z",
68+
"accountId": "9ce0cc70-9c04-48f7-a3dc-d9c1496ac229",
69+
"from": {
70+
"assetType": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501",
71+
"amount": "1000000000"
72+
},
73+
"to": {
74+
"assetType": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
75+
"amount": "161180000"
76+
}
77+
}
78+
},
79+
{
80+
"name": "transaction",
81+
"value": "gAEAChfds67pR0IYlL1XKFGjzC+zBZIA7vT1dj7nUBTUwAs7rBn3hrWmXImk+UetGwvWumZpcEhF+xT5THD5os2Svp67H8GdfJ4jkOslaYOff/X0SeF33Cha5Ij9DKlo3QaosfIhtgnDmdJAVdjfmyv5dEGsSWgYEYPaqW8v8hSJozhYIzcQa4p5MinFjNMq4vDm+n249hE5Vmwe+Adkq87GTDegPbdaVhuqlrT5hZnIkdcW67eCFvm9ZzXM9jeWm2GwnLBGfnLp6qHLD5IgtqLXr5clzwoa2ns4KdysosGA2yFHt2dBBA/kB6qwUEagfnGzH2GY12XE3va/gn3W4Loqy/D+gP5PMjWwL4nGY8i3EGxqLHt/i3x/EskcO1ftQjYqQtqMVVDE+U/Vngb6x6+HbBOGrDQOx73j0k813TrK9dmptLgHDBoOIz6tGmWT+r9wa3YtqouLIv01/IKynGlc4TnE0M2MheikqA6gkuj1PhXVDgPc7eSLCseVMCy/WUgTmKbXmnZ/QcH3X8YTJ//GR4yvL7LCHdfHPhS8B+4hpoFusQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKD0N0oI1T+8K47DiJ9N82JyiZvsX3fj3y3zO++Tr3FVRPixdukLDvYMw2r2DTumX5VA1ifoAVfXgkOTnLswDEoyXJY9OJInxuz0QKRSODYMLWhOZ2v8QhASOe9jb6fhZAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAC0P/on9df2SnTAmx8pWHneSwmrNt/J3VFLMhqns4zl6LwHxW5grT0/F3OC6sZUj7of0yz9kMoCs+fPoYX9znOYyUOdJ8PoWqEFw7H8n7xwkhg1P1tPfo1/6arncTl4PJkEedVb8jHAbu50xW7OaBUH/bGy3qP0jlECsc2iVrwTjwan1RcYx3TJKFZjmGkdXraLXrijm0ttXHNVWyEAAAAA0LlMW0o1bBeKH7IjSDuI9G0a+7kgVaW9ubbU+rklS9sFDgIWFAkAi8sXoE0wggARAAUCuIEWABEACAMgoQcAAAAAEAYACAATDS0BARVDLQ8ABgUECCoTFRUSFSslKyIjBQooKiQrDy0tKSsMAwEVKyErHB0KBygsHisPLS0pKx8gFScPJhsHBBcZGi0JGAILFSzBIJszQdacgQMDAAAAJmQAASZkAQIaZAIDQEIPAAAAAAC78khRAQAAADIAAANOgLKSmCyUmuksqMclFoVdtmiFizz7/yF11zNd6VSAxgUkIB4dIwIhIrYRAfelfqMdEh4JHXx6VS3GXpyeWhNKQlBsx9m2I8c+BhMSEBEUWgDdfctSzc+t7n0tohMIoz7S6USQkKhKDRCUSx6C3SjhJQQJAg8EBgoMCwYQBw=="
82+
},
83+
{
84+
"name": "signature",
85+
"value": "DuvWiKG54eKcB1s777TgaZWtF9R23sVuEaugRufdXyqhCf6owy5hYqxeL8sa6zoCPVQu3NxxhjWuz91CKgLE6SB"
86+
}
87+
],
88+
"result": {
89+
"name": "signAndSendTransactionWithIntentResult",
90+
"value": {
91+
"signature": "DuvWiKG54eKcB1s777TgaZWtF9R23sVuEaugRufdXyqhCf6owy5hYqxeL8sa6zoCPVQu3NxxhjWuz91CKgLE6SB",
92+
"intent": {
93+
"id": "c76b3834-b9a8-4dfd-91da-72c26a216fb2",
94+
"timestamp": "2021-01-01T00:00:00Z",
95+
"accountId": "9ce0cc70-9c04-48f7-a3dc-d9c1496ac229",
96+
"from": {
97+
"assetType": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501",
98+
"amount": "1000000000"
99+
},
100+
"to": {
101+
"assetType": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
102+
"amount": "161180000"
103+
}
104+
}
105+
}
106+
}
107+
}
108+
]
109+
}
110+
],
111+
"components": {
112+
"schemas": {
113+
"Iso8601Timestamp": {
114+
"type": "string",
115+
"format": "date-time",
116+
"description": "An ISO 8601 timestamp",
117+
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$",
118+
"examples": ["2021-01-01T00:00:00Z", "2021-01-01T00:00:00.000Z"]
119+
},
120+
"UuidV4": {
121+
"type": "string",
122+
"format": "uuid",
123+
"description": "A UUID version 4 identifier",
124+
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
125+
"examples": [
126+
"550e8400-e29b-41d4-a716-446655440000",
127+
"6ba7b810-9dad-11d1-80b4-00c04fd430c8"
128+
]
129+
},
130+
"Intent": {
131+
"type": "object",
132+
"description": "Represents a user's intent to perform a swap or bridge operation",
133+
"properties": {
134+
"id": {
135+
"$ref": "#/components/schemas/UuidV4",
136+
"description": "Unique identifier for the intent"
137+
},
138+
"timestamp": {
139+
"$ref": "#/components/schemas/Iso8601Timestamp",
140+
"description": "ISO 8601 timestamp when the intent was created"
141+
},
142+
"accountId": {
143+
"type": "string",
144+
"description": "The identifier of the account to sign the transaction with"
145+
},
146+
"from": {
147+
"$ref": "#/components/schemas/AssetAmount",
148+
"description": "The asset and amount the user wants to send"
149+
},
150+
"to": {
151+
"$ref": "#/components/schemas/AssetAmount",
152+
"description": "The asset and amount the user expects to receive"
153+
}
154+
},
155+
"required": ["id", "timestamp", "accountId", "from", "to"],
156+
"additionalProperties": false
157+
},
158+
"AssetAmount": {
159+
"type": "object",
160+
"description": "Represents an asset and its amount",
161+
"properties": {
162+
"assetType": {
163+
"type": "string",
164+
"description": "CAIP-19 asset type (e.g., 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501' for SOL)"
165+
},
166+
"amount": {
167+
"type": "string",
168+
"description": "Amount in the asset's smallest unit (e.g., lamports for SOL)"
169+
}
170+
},
171+
"required": ["assetType", "amount"],
172+
"additionalProperties": false
173+
},
174+
"SignAndSendTransactionWithIntentResult": {
175+
"type": "object",
176+
"description": "Result of signing and sending a transaction with intent",
177+
"properties": {
178+
"signature": {
179+
"type": "string",
180+
"description": "The transaction signature returned after successful execution on Solana"
181+
},
182+
"intent": {
183+
"$ref": "#/components/schemas/Intent",
184+
"description": "The original intent that was fulfilled, returned for assurance that the intent was fulfilled"
185+
}
186+
},
187+
"required": ["signature", "intent"],
188+
"additionalProperties": false
189+
}
190+
}
191+
}
192+
}

packages/snap/src/core/handlers/onClientRequest/ClientRequestHandler.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { type Json, type JsonRpcRequest } from '@metamask/snaps-sdk';
2+
import type { Struct } from '@metamask/superstruct';
23
import { assert, enums } from '@metamask/superstruct';
34

45
import type { ILogger } from '../../utils/logger';
@@ -12,6 +13,8 @@ import {
1213
export class ClientRequestHandler {
1314
readonly #methodToUseCase: Record<ClientRequestMethod, ClientRequestUseCase>;
1415

16+
readonly #methodToParamsStruct: Record<ClientRequestMethod, Struct<any, any>>;
17+
1518
readonly #logger: ILogger;
1619

1720
constructor(
@@ -23,6 +26,11 @@ export class ClientRequestHandler {
2326
signAndSendTransactionWithIntentUseCase,
2427
};
2528

29+
this.#methodToParamsStruct = {
30+
[ClientRequestMethod.SignAndSendTransactionWithIntent]:
31+
SignAndSendTransactionWithIntentParamsStruct,
32+
};
33+
2634
this.#logger = logger;
2735
}
2836

@@ -39,18 +47,10 @@ export class ClientRequestHandler {
3947
const { method, params } = request;
4048
assert(method, enums(Object.values(ClientRequestMethod)));
4149

42-
// Parse and validate parameters based on method
43-
let parsedParams: any;
44-
switch (method) {
45-
case ClientRequestMethod.SignAndSendTransactionWithIntent:
46-
assert(params, SignAndSendTransactionWithIntentParamsStruct);
47-
parsedParams = params;
48-
break;
49-
default:
50-
throw new Error(`Unsupported method: ${method}`);
51-
}
50+
const paramsStruct = this.#methodToParamsStruct[method];
51+
assert(params, paramsStruct);
5252

5353
const useCase = this.#methodToUseCase[method];
54-
return useCase.execute(parsedParams);
54+
return useCase.execute(params);
5555
}
5656
}

packages/snap/src/core/handlers/onClientRequest/SignAndSendTransactionWithIntentUseCase.ts

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { SolMethod } from '@metamask/keyring-api';
2-
import { assert, object } from '@metamask/superstruct';
3-
import type { Json } from '@metamask/utils';
2+
import { parseCaipAssetType, type Json } from '@metamask/utils';
43

5-
import { Network } from '../../constants/solana';
64
import type { WalletService } from '../../services/wallet/WalletService';
75
import type { ILogger } from '../../utils/logger';
86
import type { SolanaKeyring } from '../onKeyringRequest/Keyring';
97
import type {
108
ClientRequestUseCase,
9+
Intent,
1110
SignAndSendTransactionWithIntentParams,
1211
} from './types';
1312

@@ -31,8 +30,8 @@ export class SignAndSendTransactionWithIntentUseCase
3130
}
3231

3332
/**
34-
* Handles the signAndSendTransactionWithIntent method for client requests.
35-
* This allows swap/bridge transactions to be executed without user confirmation
33+
* Handles the request client_signAndSendTransactionWithIntent.
34+
* This allows transactions to be executed without user confirmation
3635
* when they match a verified intent from the backend.
3736
*
3837
* @param params - The validated parameters containing intent, transaction, and signature.
@@ -44,39 +43,37 @@ export class SignAndSendTransactionWithIntentUseCase
4443
params,
4544
);
4645

47-
const { intent, tx, signature } = params;
46+
const { intent, transaction, signature } = params;
47+
const { accountId, from } = intent;
4848

49-
// Verify that the backend signed the intent (and transaction?)
50-
// to ensure the transaction came from our backend and matches the user's intent
49+
// Verify that the backend signed the payload { intent, transaction }
50+
// to ensure the transaction came from our backend.
5151
const isValidSignature = await this.#verifyBackendSignature(
5252
intent,
53-
tx,
53+
transaction,
5454
signature,
5555
);
5656

5757
if (!isValidSignature) {
5858
throw new Error('Invalid backend signature');
5959
}
6060

61-
// Verify that the transaction actually performs the swap/bridge
62-
// described in the intent (correct amounts, assets, etc.)
61+
// Verify that the transaction actually performs what is described in the intent
62+
// (correct amounts, assets, etc.)
6363
const transactionMatchesIntent = await this.#verifyTransactionMatchesIntent(
6464
intent,
65-
tx,
65+
transaction,
6666
);
6767

6868
if (!transactionMatchesIntent) {
6969
throw new Error('Transaction does not match intent');
7070
}
7171

72-
// Get the user's account
73-
// TODO: How do we know which account to use? For now, we'll use the first account
74-
const account = (await this.#keyring.listAccounts())[0]; // TODO: We should move account CRUDs to an AccountService
75-
assert(account, object());
72+
// Get the user's account from the accountId specified in the intent
73+
const account = await this.#keyring.getAccountOrThrow(accountId);
7674

77-
// TODO: How do we know the correct network?
78-
// For now, defaulting to mainnet
79-
const scope: Network = Network.Mainnet;
75+
// Get the scope from the intent
76+
const scope = parseCaipAssetType(from.assetType).chainId;
8077

8178
// Sign and send the transaction
8279
const response = await this.#walletService.signAndSendTransaction(account, {
@@ -86,7 +83,7 @@ export class SignAndSendTransactionWithIntentUseCase
8683
request: {
8784
method: SolMethod.SignAndSendTransaction,
8885
params: {
89-
transaction: tx,
86+
transaction,
9087
scope,
9188
account: {
9289
address: account.address,
@@ -111,13 +108,13 @@ export class SignAndSendTransactionWithIntentUseCase
111108
* This ensures the request came from our trusted backend.
112109
*
113110
* @param intent - The swap/bridge intent.
114-
* @param tx - The base64 encoded transaction.
111+
* @param transaction - The base64 encoded transaction.
115112
* @param signature - The backend signature to verify.
116113
* @returns True if the signature is valid.
117114
*/
118115
async #verifyBackendSignature(
119116
intent: any,
120-
tx: string,
117+
transaction: string,
121118
signature: string,
122119
): Promise<boolean> {
123120
// TODO: Implement actual signature verification
@@ -138,12 +135,12 @@ export class SignAndSendTransactionWithIntentUseCase
138135
* This ensures the transaction matches what the user expects.
139136
*
140137
* @param intent - The swap/bridge intent.
141-
* @param tx - The base64 encoded transaction.
138+
* @param transaction - The base64 encoded transaction.
142139
* @returns True if the transaction matches the intent.
143140
*/
144141
async #verifyTransactionMatchesIntent(
145-
intent: any,
146-
tx: string,
142+
intent: Intent,
143+
transaction: string,
147144
): Promise<boolean> {
148145
// TODO: Implement transaction verification
149146
// This should:

0 commit comments

Comments
 (0)