Skip to content
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

Chore/sign message clarificaitons #33

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3ea319e
upd readme
tsmbl Sep 30, 2024
5ee2a0b
add utility fns for sign message
tsmbl Sep 30, 2024
f003069
configure workspaces
tsmbl Sep 30, 2024
2b290e3
fix contributors github
tsmbl Sep 30, 2024
5705532
solana-actions: fix build errors
tsmbl Oct 1, 2024
9f2639c
solana-actions: add tests for signMessageData
tsmbl Oct 1, 2024
52d808d
actions-spec: update readme
tsmbl Oct 1, 2024
2418e38
cleanup
tsmbl Oct 1, 2024
65888c2
chains ids is optional verification param
tsmbl Oct 1, 2024
beb8c77
normalize domains before comparing
tsmbl Oct 2, 2024
8f2d2d1
Update packages/actions-spec/README.md
tsmbl Oct 2, 2024
7f5782a
Update packages/actions-spec/README.md
tsmbl Oct 2, 2024
b74b60a
Update packages/actions-spec/README.md
tsmbl Oct 2, 2024
8722f60
Update packages/actions-spec/README.md
tsmbl Oct 2, 2024
6008f87
Update packages/solana-actions/src/signMessageData.ts
tsmbl Oct 2, 2024
c759a25
Update packages/solana-actions/test/signMessageData.test.ts
tsmbl Oct 2, 2024
c338aa2
Update packages/solana-actions/test/signMessageData.test.ts
tsmbl Oct 2, 2024
bfd2494
Update packages/solana-actions/test/signMessageData.test.ts
tsmbl Oct 2, 2024
6173eb0
Update packages/solana-actions/test/signMessageData.test.ts
tsmbl Oct 2, 2024
4c6c45e
Update packages/solana-actions/test/signMessageData.test.ts
tsmbl Oct 2, 2024
5ffe0b6
Update packages/solana-actions/src/signMessageData.ts
tsmbl Oct 2, 2024
52a6eb2
Update packages/solana-actions/test/signMessageData.test.ts
tsmbl Oct 2, 2024
a24524e
Update packages/solana-actions/test/signMessageData.test.ts
tsmbl Oct 2, 2024
819a19b
Update packages/solana-actions/test/signMessageData.test.ts
tsmbl Oct 2, 2024
ac9b005
review fixes
tsmbl Oct 2, 2024
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
10,047 changes: 9,324 additions & 723 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
"engines": {
"node": ">=16"
},
"workspaces": [
"packages/*"
],
"scripts": {
"prettier": "npx prettier --write '{*,**/*}.{ts,tsx,js,jsx,css,json}'"
},
Expand Down
42 changes: 42 additions & 0 deletions packages/actions-spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,48 @@ export type SignMessageData = {
When received by the blink client, the user should be shown the plaintext `data`
value and prompted to sign it with their wallet to generate a `signature`.

The `data` can be a plaintext string or a structured `SignMessageData` object.

When using `SignMessageData`, it must be formatted as a standardized, human-readable plaintext suitable for signing.
Both the client and server must generate the message using the same method to ensure proper verification.
The following template must be used by both the Action API and the client to format `SignMessageData`:

```
${domain} wants you to sign a message with your account:
${address}

${statement}

Chain ID: ${chainId}
Nonce: ${nonce}
Issued At: ${issuedAt}
```

If `chainId` is not provided, the `Chain ID` line should be omitted from the message to be signed.

Client should not prefix, suffix or otherwise modify the `SignMessageData` value before signing it.
Client should perform validation on the `SignMessageData` before signing to ensure that it meets expected criteria and to prevent potential security issues.

The following function illustrates how to create a human-readable message text from `SignMessageData`:

```ts
export function createSignMessageText(input: SignMessageData): string {
let message = `${input.domain} wants you to sign a message with your account:\n`;
message += `${input.address}`;
message += `\n\n${input.statement}`;
const fields: string[] = [];

if (input.chainId) {
fields.push(`Chain ID: ${input.chainId}`);
}
fields.push(`Nonce: ${input.nonce}`);
fields.push(`Issued At: ${input.issuedAt}`);
message += `\n\n${fields.join("\n")}`;

return message;
}
```

After signing, the blink client will continue the chain-of-actions by making a
POST request to the provided `PostNextActionLink` endpoint with a payload
similar to the normal `ActionPostRequest` fields (see
Expand Down
2 changes: 1 addition & 1 deletion packages/actions-spec/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
},
{
"name": "Alexey Tsymbal",
"url": "https://github.com/Baulore"
"url": "https://github.com/tsmbl"
},
{
"name": "Filipp Sher",
Expand Down
4 changes: 2 additions & 2 deletions packages/solana-actions/src/createPostResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
createActionIdentifierInstruction,
getActionIdentityFromEnv,
} from "./actionIdentity.js";
import { ActionPostResponse } from "@solana/actions-spec";
import { ActionPostResponse, TransactionResponse } from "@solana/actions-spec";

/**
* Thrown when the Action POST response cannot be created.
Expand All @@ -29,7 +29,7 @@ export interface CreateActionPostResponseArgs<
TransactionType = Transaction | VersionedTransaction,
> {
/** POST response fields per the Solana Actions spec. */
fields: Omit<ActionPostResponse, "transaction"> & {
fields: Omit<TransactionResponse, "transaction"> & {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this works for now since the createPostResponse does not yet support the recently added optional transactions and external links are not supported at all in the current version of @solana/actions

either way, this will also require updating the spec package inside the @solana/actions package.
I just opened an issue to track it and will complete the update in a different PR: #34

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, just wanted to address existing build errors

thanks for creating the issue

/** Solana transaction to be base64 encoded. */
transaction: TransactionType;
};
Expand Down
8 changes: 6 additions & 2 deletions packages/solana-actions/src/fetchTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ActionPostRequest, ActionPostResponse } from "@solana/actions-spec";
import {
ActionPostRequest,
ActionPostResponse,
TransactionResponse,
} from "@solana/actions-spec";
import { Commitment, Connection, PublicKey } from "@solana/web3.js";
import { Transaction } from "@solana/web3.js";
import fetch from "cross-fetch";
Expand Down Expand Up @@ -49,7 +53,7 @@ export async function fetchTransaction(
body: JSON.stringify(fields),
});

const json = (await response.json()) as ActionPostResponse;
const json = (await response.json()) as TransactionResponse;
if (!json?.transaction) throw new FetchActionError("missing transaction");
if (typeof json.transaction !== "string")
throw new FetchActionError("invalid transaction");
Expand Down
1 change: 1 addition & 0 deletions packages/solana-actions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from "./fetchTransaction.js";
export * from "./findReference.js";
export * from "./createPostResponse.js";
export * from "./actionIdentity.js";
export * from "./signMessageData.js";
147 changes: 147 additions & 0 deletions packages/solana-actions/src/signMessageData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import type { SignMessageData } from "@solana/actions-spec";

export interface SignMessageVerificationOptions {
expectedAddress?: string;
expectedDomains?: string[];
expectedChainIds?: string[];
issuedAtThreshold?: number;
}

export enum SignMessageVerificationErrorType {
ADDRESS_MISMATCH = "ADDRESS_MISMATCH",
DOMAIN_MISMATCH = "DOMAIN_MISMATCH",
CHAIN_ID_MISMATCH = "CHAIN_ID_MISMATCH",
ISSUED_TOO_FAR_IN_THE_PAST = "ISSUED_TOO_FAR_IN_THE_PAST",
ISSUED_TOO_FAR_IN_THE_FUTURE = "ISSUED_TOO_FAR_IN_THE_FUTURE",
INVALID_DATA = "INVALID_DATA",
}

const DOMAIN =
"(?<domain>[^\\n]+?) wants you to sign a message with your account:\\n";
const ADDRESS = "(?<address>[^\\n]+)(?:\\n|$)";
const STATEMENT = "(?:\\n(?<statement>[\\S\\s]*?)(?:\\n|$))";
const CHAIN_ID = "(?:\\nChain ID: (?<chainId>[^\\n]+))?";
const NONCE = "\\nNonce: (?<nonce>[^\\n]+)";
const ISSUED_AT = "\\nIssued At: (?<issuedAt>[^\\n]+)";
const FIELDS = `${CHAIN_ID}${NONCE}${ISSUED_AT}`;
const MESSAGE = new RegExp(`^${DOMAIN}${ADDRESS}${STATEMENT}${FIELDS}\\n*$`);

/**
* Create a human-readable message text for the user to sign.
*
* @param input The data to be signed.
* @returns The message text.
*/
export function createSignMessageText(input: SignMessageData): string {
let message = `${input.domain} wants you to sign a message with your account:\n`;
message += `${input.address}`;
message += `\n\n${input.statement}`;
const fields: string[] = [];

if (input.chainId) {
fields.push(`Chain ID: ${input.chainId}`);
}
fields.push(`Nonce: ${input.nonce}`);
fields.push(`Issued At: ${input.issuedAt}`);
message += `\n\n${fields.join("\n")}`;

return message;
}

/**
* Parse the sign message text to extract the data to be signed.
* @param text The message text to be parsed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param text The message text to be parsed.
* @param string The message text to be parsed.

Copy link
Collaborator Author

@tsmbl tsmbl Oct 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think text it's correct in doc, it matches parameter name

*/
export function parseSignMessageText(text: string): SignMessageData | null {
const match = MESSAGE.exec(text);
if (!match) return null;
const groups = match.groups;
if (!groups) return null;

return {
domain: groups.domain,
address: groups.address,
statement: groups.statement,
nonce: groups.nonce,
chainId: groups.chainId,
issuedAt: groups.issuedAt,
};
}

/**
* Verify the sign message data before signing.
* @param data The data to be signed.
* @param opts Options for verification, including the expected address, chainId, issuedAt, and domains.
*
* @returns An array of errors if the verification fails.
*/
export function verifySignMessageData(
data: SignMessageData,
opts: SignMessageVerificationOptions,
) {
if (
!data.address ||
!data.domain ||
!data.issuedAt ||
!data.nonce ||
!data.statement
) {
return [SignMessageVerificationErrorType.INVALID_DATA];
}

try {
const {
expectedAddress,
expectedChainIds,
issuedAtThreshold,
expectedDomains,
} = opts;
const errors: SignMessageVerificationErrorType[] = [];
const now = Date.now();

// verify if parsed address is same as the expected address
if (expectedAddress && data.address !== expectedAddress) {
errors.push(SignMessageVerificationErrorType.ADDRESS_MISMATCH);
}

if (expectedDomains) {
const expectedDomainsNormalized = expectedDomains.map(normalizeDomain);
const normalizedDomain = normalizeDomain(data.domain);

if (!expectedDomainsNormalized.includes(normalizedDomain)) {
errors.push(SignMessageVerificationErrorType.DOMAIN_MISMATCH);
}
}

if (
expectedChainIds &&
data.chainId &&
!expectedChainIds.includes(data.chainId)
) {
errors.push(SignMessageVerificationErrorType.CHAIN_ID_MISMATCH);
}

if (issuedAtThreshold !== undefined) {
const iat = Date.parse(data.issuedAt);
if (Math.abs(iat - now) > issuedAtThreshold) {
if (iat < now) {
errors.push(
SignMessageVerificationErrorType.ISSUED_TOO_FAR_IN_THE_PAST,
);
} else {
errors.push(
SignMessageVerificationErrorType.ISSUED_TOO_FAR_IN_THE_FUTURE,
);
}
}
}

return errors;
} catch (e) {
return [SignMessageVerificationErrorType.INVALID_DATA];
}
}

function normalizeDomain(domain: string): string {
return domain.replace(/^www\./, "");
}
Loading