From d5fb4b85b8d8bd7803a797f6da83e1540b1c9242 Mon Sep 17 00:00:00 2001 From: Sam Hogan Date: Sun, 28 Aug 2022 11:14:32 -0600 Subject: [PATCH 01/20] Update SPEC.md --- SPEC.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SPEC.md b/SPEC.md index 5fa8392c..e710b4df 100644 --- a/SPEC.md +++ b/SPEC.md @@ -151,10 +151,10 @@ The wallet should display the domain of the URL as the request is being made. If The wallet must handle HTTP [client error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses), [server error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses), and [redirect responses](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages). The application must respond with these, or with an HTTP `OK` JSON response with a body of ```json -{"transaction":""} +{"transaction":""} ``` -The `` value must be a base64-encoded [serialized transaction](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#serialize). The wallet must base64-decode the transaction and [deserialize it](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#from). +The `` value must be a base64-encoded [serialized transaction](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#serialize), or `null`. The wallet must base64-decode any non-null transaction and [deserialize it](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#from). The application may respond with a partially or fully signed transaction. The wallet must validate the transaction as **untrusted**. @@ -182,7 +182,7 @@ The application may also include an optional `message` field in the response bod The `` value must be a UTF-8 string that describes the nature of the transaction response. -For example, this might be the name of an item being purchased, a discount applied to the purchase, or a thank you note. The wallet should display the value to the user. +For example, this might be the name of an item being purchased, a discount applied to the purchase, an error, or a thank you note. The wallet should display at least the first 80 characters of any non-empty message, even if the `transaction` field is `null`. The wallet and application should allow additional fields in the request body and response body, which may be added by future specification. From 8cf32ee57cb14138512537819b339162a0d8d98a Mon Sep 17 00:00:00 2001 From: Sam Heutmaker Date: Tue, 11 Oct 2022 14:27:01 -0600 Subject: [PATCH 02/20] preliminary code changes for sign message --- core/package-lock.json | 4 +- core/src/encodeURL.ts | 10 +-- core/src/fetchInteraction.ts | 138 +++++++++++++++++++++++++++++++++++ core/src/parseURL.ts | 10 +-- 4 files changed, 150 insertions(+), 12 deletions(-) create mode 100644 core/src/fetchInteraction.ts diff --git a/core/package-lock.json b/core/package-lock.json index fb9c4f7f..5c422395 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solana/pay", - "version": "0.2.2", + "version": "0.2.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@solana/pay", - "version": "0.2.2", + "version": "0.2.4", "license": "Apache-2.0", "dependencies": { "@solana/qr-code-styling": "^1.6.0-beta.0", diff --git a/core/src/encodeURL.ts b/core/src/encodeURL.ts index 2689d637..d65befef 100644 --- a/core/src/encodeURL.ts +++ b/core/src/encodeURL.ts @@ -2,9 +2,9 @@ import { SOLANA_PROTOCOL } from './constants.js'; import type { Amount, Label, Memo, Message, Recipient, References, SPLToken } from './types.js'; /** - * Fields of a Solana Pay transaction request URL. + * Fields of a Solana Pay transaction or message signing request URL. */ -export interface TransactionRequestURLFields { +export interface InteractiveRequestURLFields { /** `link` in the [Solana Pay spec](https://github.com/solana-labs/solana-pay/blob/master/SPEC.md#link). */ link: URL; /** `label` in the [Solana Pay spec](https://github.com/solana-labs/solana-pay/blob/master/SPEC.md#label-1). */ @@ -38,11 +38,11 @@ export interface TransferRequestURLFields { * * @param fields Fields to encode in the URL. */ -export function encodeURL(fields: TransactionRequestURLFields | TransferRequestURLFields): URL { - return 'link' in fields ? encodeTransactionRequestURL(fields) : encodeTransferRequestURL(fields); +export function encodeURL(fields: InteractiveRequestURLFields | TransferRequestURLFields): URL { + return 'link' in fields ? encodeInteractiveRequestURL(fields) : encodeTransferRequestURL(fields); } -function encodeTransactionRequestURL({ link, label, message }: TransactionRequestURLFields): URL { +function encodeInteractiveRequestURL({ link, label, message }: InteractiveRequestURLFields): URL { // Remove trailing slashes const pathname = link.search ? encodeURIComponent(String(link).replace(/\/\?/, '?')) diff --git a/core/src/fetchInteraction.ts b/core/src/fetchInteraction.ts new file mode 100644 index 00000000..8e694037 --- /dev/null +++ b/core/src/fetchInteraction.ts @@ -0,0 +1,138 @@ +import type { Commitment, Connection, PublicKey } from '@solana/web3.js'; +import { Transaction } from '@solana/web3.js'; +import fetch from 'cross-fetch'; +import { toUint8Array } from 'js-base64'; +import nacl from 'tweetnacl'; + +/** + * Thrown when a transaction response can't be fetched. + */ +export class FetchInteractionError extends Error { + name = 'FetchInteractionError'; +} + +export interface FetchTransactionResponse { + transaction: Transaction; + message?: string; +} + +export interface FetchSignMessageResponse { + data: string; + state: string; + message?: string; +} + +export interface FetchInteractionErrorResponse { + message?: string; +} + +export type FetchInteractionResponse = + | FetchTransactionResponse + | FetchSignMessageResponse + | FetchInteractionErrorResponse; + +interface FetchInteractionServerResponse { + transaction?: string; + data?: string; + state?: string; + message?: string; +} + +export const isTransactionResponse = (value: FetchInteractionResponse): boolean => { + return 'transaction' in value; +}; + +export const isSignMessageResponse = (value: FetchInteractionResponse): boolean => { + return 'data' in value && `state` in value; +}; + +const isBase64 = (value: string): boolean => { + return /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/.test(value); +}; + +/** + * Fetch a transaction from a Solana Pay transaction request link. + * + * @param connection - A connection to the cluster. + * @param account - Account that may sign the transaction. + * @param link - `link` in the [Solana Pay spec](https://github.com/solana-labs/solana-pay/blob/master/SPEC.md#link). + * @param options - Options for `getLatestBlockhash`. + * + * @throws {FetchInteractionError} + */ +export async function fetchInteraction( + connection: Connection, + account: PublicKey, + link: string | URL, + { commitment }: { commitment?: Commitment } = {} +): Promise { + const response = await fetch(String(link), { + method: 'POST', + mode: 'cors', + cache: 'no-cache', + credentials: 'omit', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ account }), + }); + + const json: FetchInteractionServerResponse = await response.json(); + + if (!json.transaction && !json.state && !json.data && !json.message) { + throw new FetchInteractionError('invalid response'); + } + + /** + * Transaction Request validation and parsing + */ + + if (json.transaction) { + if (json.data || json.state) throw new FetchInteractionError('invalid transaction response'); + if (typeof json.transaction !== 'string') throw new FetchInteractionError('invalid transaction'); + const transaction = Transaction.from(toUint8Array(json.transaction)); + const { signatures, feePayer, recentBlockhash } = transaction; + + if (signatures.length) { + if (!feePayer) throw new FetchInteractionError('missing fee payer'); + if (!feePayer.equals(signatures[0].publicKey)) throw new FetchInteractionError('invalid fee payer'); + if (!recentBlockhash) throw new FetchInteractionError('missing recent blockhash'); + + // A valid signature for everything except `account` must be provided. + const message = transaction.serializeMessage(); + for (const { signature, publicKey } of signatures) { + if (signature) { + if (!nacl.sign.detached.verify(message, signature, publicKey.toBuffer())) + throw new FetchInteractionError('invalid signature'); + } else if (publicKey.equals(account)) { + // If the only signature expected is for `account`, ignore the recent blockhash in the transaction. + if (signatures.length === 1) { + transaction.recentBlockhash = (await connection.getLatestBlockhash(commitment)).blockhash; + } + } else { + throw new FetchInteractionError('missing signature'); + } + } + } else { + // Ignore the fee payer and recent blockhash in the transaction and initialize them. + transaction.feePayer = account; + transaction.recentBlockhash = (await connection.getLatestBlockhash(commitment)).blockhash; + } + + return { transaction, message: json.message }; + } + + /** + * Sign Message Request validation and parsing + */ + + if (json.data) { + if (json.transaction) throw new FetchInteractionError('invalid sign message response'); + if (typeof json.state !== 'string') throw new FetchInteractionError('invalid state field'); + if (!isBase64(json.data)) throw new FetchInteractionError('invalid data field'); + return { data: json.data, state: json.state, message: json.message }; + } + + return { message: json.message }; +} diff --git a/core/src/parseURL.ts b/core/src/parseURL.ts index 1a5dc8a0..9a686e0e 100644 --- a/core/src/parseURL.ts +++ b/core/src/parseURL.ts @@ -4,9 +4,9 @@ import { HTTPS_PROTOCOL, SOLANA_PROTOCOL } from './constants.js'; import type { Amount, Label, Link, Memo, Message, Recipient, Reference, SPLToken } from './types.js'; /** - * A Solana Pay transaction request URL. + * A Solana Pay interactive request URL. */ -export interface TransactionRequestURL { +export interface InteractiveRequestURL { /** `link` in the [Solana Pay spec](https://github.com/solana-labs/solana-pay/blob/master/SPEC.md#link). */ link: Link; /** `label` in the [Solana Pay spec](https://github.com/solana-labs/solana-pay/blob/master/SPEC.md#label-1). */ @@ -49,7 +49,7 @@ export class ParseURLError extends Error { * * @throws {ParseURLError} */ -export function parseURL(url: string | URL): TransactionRequestURL | TransferRequestURL { +export function parseURL(url: string | URL): InteractiveRequestURL | TransferRequestURL { if (typeof url === 'string') { if (url.length > 2048) throw new ParseURLError('length invalid'); url = new URL(url); @@ -58,10 +58,10 @@ export function parseURL(url: string | URL): TransactionRequestURL | TransferReq if (url.protocol !== SOLANA_PROTOCOL) throw new ParseURLError('protocol invalid'); if (!url.pathname) throw new ParseURLError('pathname missing'); - return /[:%]/.test(url.pathname) ? parseTransactionRequestURL(url) : parseTransferRequestURL(url); + return /[:%]/.test(url.pathname) ? parseInteractiveRequestURL(url) : parseTransferRequestURL(url); } -function parseTransactionRequestURL({ pathname, searchParams }: URL): TransactionRequestURL { +function parseInteractiveRequestURL({ pathname, searchParams }: URL): InteractiveRequestURL { const link = new URL(decodeURIComponent(pathname)); if (link.protocol !== HTTPS_PROTOCOL) throw new ParseURLError('link invalid'); From 4579f7b6dafeb54df8acb635cfda96451d6ec8af Mon Sep 17 00:00:00 2001 From: Sam Heutmaker Date: Wed, 12 Oct 2022 11:44:31 -0600 Subject: [PATCH 03/20] create sendSignedData --- core/src/fetchInteraction.ts | 15 ++++++-- core/src/sendSignedData.ts | 70 ++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 core/src/sendSignedData.ts diff --git a/core/src/fetchInteraction.ts b/core/src/fetchInteraction.ts index 8e694037..e5b4cc4e 100644 --- a/core/src/fetchInteraction.ts +++ b/core/src/fetchInteraction.ts @@ -120,7 +120,10 @@ export async function fetchInteraction( transaction.recentBlockhash = (await connection.getLatestBlockhash(commitment)).blockhash; } - return { transaction, message: json.message }; + return { + transaction, + message: json.message, + }; } /** @@ -131,8 +134,14 @@ export async function fetchInteraction( if (json.transaction) throw new FetchInteractionError('invalid sign message response'); if (typeof json.state !== 'string') throw new FetchInteractionError('invalid state field'); if (!isBase64(json.data)) throw new FetchInteractionError('invalid data field'); - return { data: json.data, state: json.state, message: json.message }; + return { + data: json.data, + state: json.state, + message: json.message, + }; } - return { message: json.message }; + return { + message: json.message, + }; } diff --git a/core/src/sendSignedData.ts b/core/src/sendSignedData.ts new file mode 100644 index 00000000..28ee3784 --- /dev/null +++ b/core/src/sendSignedData.ts @@ -0,0 +1,70 @@ +import type { Commitment, Connection, PublicKey } from '@solana/web3.js'; +import type { Transaction } from '@solana/web3.js'; +import fetch from 'cross-fetch'; + +/** + * Thrown when response is invalid + */ +export class SendSignedDataError extends Error { + name = 'SendSignedDataError'; +} + +export interface FetchTransactionResponse { + transaction: Transaction; + message?: string; +} + +export interface FetchSignMessageResponse { + data: string; + state: string; + message?: string; +} + +export interface SendSignedDataErrorResponse { + message?: string; +} + +export type SendSignedDataResponse = { + success?: boolean; +}; + +/** + * Fetch a transaction from a Solana Pay transaction request link. + * + * @param account - Account that signed the data + * @param state - MAC value that was returned from the server during the the inital request. + * @param signature - The signature from signing the data. + * @param link - `link` in the [Solana Pay spec](https://github.com/solana-labs/solana-pay/blob/master/SPEC.md#link). + * + * @throws {SendSignedDataError} + */ +export async function fetchInteraction( + account: PublicKey, + state: string, + signature: string, + link: string | URL +): Promise { + const response = await fetch(String(link), { + method: 'POST', + mode: 'cors', + cache: 'no-cache', + credentials: 'omit', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + account, + state, + signature, + }), + }); + + const json: SendSignedDataResponse = await response.json(); + + if (typeof json.success === 'undefined') { + throw new SendSignedDataError('invalid response'); + } + + return json; +} From 5371124a7e8cf6e2f479c0aa77e16ba53f77920b Mon Sep 17 00:00:00 2001 From: Sam Heutmaker Date: Wed, 12 Oct 2022 11:51:32 -0600 Subject: [PATCH 04/20] update fetchTransaction to use fetchInteraction internally --- core/src/fetchInteraction.ts | 4 +-- core/src/fetchTransaction.ts | 53 ++++-------------------------------- 2 files changed, 8 insertions(+), 49 deletions(-) diff --git a/core/src/fetchInteraction.ts b/core/src/fetchInteraction.ts index e5b4cc4e..9e2ce804 100644 --- a/core/src/fetchInteraction.ts +++ b/core/src/fetchInteraction.ts @@ -38,11 +38,11 @@ interface FetchInteractionServerResponse { message?: string; } -export const isTransactionResponse = (value: FetchInteractionResponse): boolean => { +export const isTransactionResponse = (value: FetchInteractionResponse): value is FetchTransactionResponse => { return 'transaction' in value; }; -export const isSignMessageResponse = (value: FetchInteractionResponse): boolean => { +export const isSignMessageResponse = (value: FetchInteractionResponse): value is FetchSignMessageResponse => { return 'data' in value && `state` in value; }; diff --git a/core/src/fetchTransaction.ts b/core/src/fetchTransaction.ts index e371b12b..637d08b0 100644 --- a/core/src/fetchTransaction.ts +++ b/core/src/fetchTransaction.ts @@ -1,8 +1,6 @@ import type { Commitment, Connection, PublicKey } from '@solana/web3.js'; -import { Transaction } from '@solana/web3.js'; -import fetch from 'cross-fetch'; -import { toUint8Array } from 'js-base64'; -import nacl from 'tweetnacl'; +import type { Transaction } from '@solana/web3.js'; +import { fetchInteraction, isTransactionResponse } from './fetchInteraction.js'; /** * Thrown when a transaction response can't be fetched. @@ -27,50 +25,11 @@ export async function fetchTransaction( link: string | URL, { commitment }: { commitment?: Commitment } = {} ): Promise { - const response = await fetch(String(link), { - method: 'POST', - mode: 'cors', - cache: 'no-cache', - credentials: 'omit', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ account }), - }); + const response = await fetchInteraction(connection, account, link, { commitment }); - const json = await response.json(); - if (!json?.transaction) throw new FetchTransactionError('missing transaction'); - if (typeof json.transaction !== 'string') throw new FetchTransactionError('invalid transaction'); - - const transaction = Transaction.from(toUint8Array(json.transaction)); - const { signatures, feePayer, recentBlockhash } = transaction; - - if (signatures.length) { - if (!feePayer) throw new FetchTransactionError('missing fee payer'); - if (!feePayer.equals(signatures[0].publicKey)) throw new FetchTransactionError('invalid fee payer'); - if (!recentBlockhash) throw new FetchTransactionError('missing recent blockhash'); - - // A valid signature for everything except `account` must be provided. - const message = transaction.serializeMessage(); - for (const { signature, publicKey } of signatures) { - if (signature) { - if (!nacl.sign.detached.verify(message, signature, publicKey.toBuffer())) - throw new FetchTransactionError('invalid signature'); - } else if (publicKey.equals(account)) { - // If the only signature expected is for `account`, ignore the recent blockhash in the transaction. - if (signatures.length === 1) { - transaction.recentBlockhash = (await connection.getRecentBlockhash(commitment)).blockhash; - } - } else { - throw new FetchTransactionError('missing signature'); - } - } - } else { - // Ignore the fee payer and recent blockhash in the transaction and initialize them. - transaction.feePayer = account; - transaction.recentBlockhash = (await connection.getRecentBlockhash(commitment)).blockhash; + if (!isTransactionResponse(response)) { + throw new FetchTransactionError('invalid response'); } - return transaction; + return response.transaction; } From f6369d77404a36fe03f171009d69188e85995bf1 Mon Sep 17 00:00:00 2001 From: Sam Heutmaker Date: Wed, 12 Oct 2022 12:06:08 -0600 Subject: [PATCH 05/20] remove unneeded validation --- core/src/fetchInteraction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/fetchInteraction.ts b/core/src/fetchInteraction.ts index 9e2ce804..0172ceea 100644 --- a/core/src/fetchInteraction.ts +++ b/core/src/fetchInteraction.ts @@ -131,9 +131,9 @@ export async function fetchInteraction( */ if (json.data) { - if (json.transaction) throw new FetchInteractionError('invalid sign message response'); if (typeof json.state !== 'string') throw new FetchInteractionError('invalid state field'); if (!isBase64(json.data)) throw new FetchInteractionError('invalid data field'); + return { data: json.data, state: json.state, From 1c580f03146744b44bbff9e09a05c7c5cdc3b711 Mon Sep 17 00:00:00 2001 From: Sam Hogan Date: Wed, 12 Oct 2022 12:07:44 -0600 Subject: [PATCH 06/20] Update SPEC.md --- SPEC.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SPEC.md b/SPEC.md index 75ded1ae..507bd470 100644 --- a/SPEC.md +++ b/SPEC.md @@ -153,10 +153,10 @@ The wallet should display the domain of the URL as the request is being made. If The wallet must handle HTTP [client error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses), [server error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses), and [redirect responses](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages). The application must respond with these, or with an HTTP `OK` JSON response with a body of ```json -{"transaction":""} +{"transaction":""} ``` -The `` value must be a base64-encoded [serialized transaction](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#serialize), or `null`. The wallet must base64-decode any non-null transaction and [deserialize it](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#from). +The `` value must be a base64-encoded [serialized transaction](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#serialize). The wallet must base64-decode the transaction and [deserialize it](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#from). The application may respond with a partially or fully signed transaction. The wallet must validate the transaction as **untrusted**. @@ -184,7 +184,7 @@ The application may also include an optional `message` field in the response bod The `` value must be a UTF-8 string that describes the nature of the transaction response. -For example, this might be the name of an item being purchased, a discount applied to the purchase, an error, or a thank you note. The wallet should display at least the first 80 characters of any non-empty message, even if the `transaction` field is `null`. +For example, this might be the name of an item being purchased, a discount applied to the purchase, or a thank you note. The wallet should display the value to the user. The wallet and application should allow additional fields in the request body and response body, which may be added by future specification. From de72cc3132ba7483196c64c964742ee69c619939 Mon Sep 17 00:00:00 2001 From: Sam Heutmaker Date: Wed, 19 Oct 2022 09:04:05 -0600 Subject: [PATCH 07/20] add type guard for error response --- core/src/fetchInteraction.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/src/fetchInteraction.ts b/core/src/fetchInteraction.ts index 0172ceea..9ef26515 100644 --- a/core/src/fetchInteraction.ts +++ b/core/src/fetchInteraction.ts @@ -46,6 +46,10 @@ export const isSignMessageResponse = (value: FetchInteractionResponse): value is return 'data' in value && `state` in value; }; +export const isErrorResponse = (value: FetchInteractionErrorResponse): value is FetchInteractionErrorResponse => { + return !isSignMessageResponse(value) && !isTransactionResponse(value); +}; + const isBase64 = (value: string): boolean => { return /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/.test(value); }; From d3b64c892f0cc7415166271c40f9253d7821d5f2 Mon Sep 17 00:00:00 2001 From: Sam Hogan Date: Wed, 19 Oct 2022 09:25:15 -0600 Subject: [PATCH 08/20] Update SPEC.md --- SPEC.md | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 122 insertions(+), 9 deletions(-) diff --git a/SPEC.md b/SPEC.md index 507bd470..89d3aa4a 100644 --- a/SPEC.md +++ b/SPEC.md @@ -95,14 +95,16 @@ solana:mvines9iiHiQTysrwkJjGf2gb9Ex9jXJX8ns3qwf2kN?amount=0.01&spl-token=EPjFWdd solana:mvines9iiHiQTysrwkJjGf2gb9Ex9jXJX8ns3qwf2kN&label=Michael ``` -## Specification: Transaction Request +## Specification: Interactive Request -A Solana Pay transaction request URL describes an interactive request for any Solana transaction. +A Solana Pay interactive request URL describes an interactive request where the parameters in the URL are used by a wallet to make an HTTP request to a remote server. Currently two types of interactive requests exist: + +1. Transaction Request: A Solana Pay transaction request URL describes an interactive request that returns any Solana transaction. +2. Sign-message Request: A Solana Pay sign-message request URL describes an interactive request that is used to verify ownership of an address. ```html solana: ``` - -The request is interactive because the parameters in the URL are used by a wallet to make an HTTP request to compose a transaction. +The initial request URL structure for both types of interactive requests are the same. As such, wallets will not know which type of interaction is being requested until the response payload is received from the server. ### Link A single `link` field is required as the pathname. The value must be a conditionally [URL-encoded](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) absolute HTTPS URL. @@ -136,6 +138,8 @@ The wallet should not cache the response except as instructed by [HTTP caching]( The wallet should display the label and render the icon image to user. +### Transction Request + #### POST Request The wallet must make an HTTP `POST` JSON request to the URL with a body of @@ -188,21 +192,130 @@ For example, this might be the name of an item being purchased, a discount appli The wallet and application should allow additional fields in the request body and response body, which may be added by future specification. -### Example +### Transation Request Example ##### URL describing a transaction request. ``` -solana:https://example.com/solana-pay +solana:https://example.com/solana-pay/transction-request +``` + +##### URL describing a transaction request with query parameters. +``` +solana:https%3A%2F%2Fexample.com%2Fsolana-pay%2Ftransaction-request%3Forder%3D12345 +``` + +##### GET Request +``` +GET /solana-pay/transaction-request?order=12345 HTTP/1.1 +Host: example.com +Connection: close +Accept: application/json +Accept-Encoding: br, gzip, deflate +``` + +##### GET Response +``` +HTTP/1.1 200 OK +Connection: close +Content-Type: application/json +Content-Length: 62 +Content-Encoding: gzip + +{"label":"Michael Vines","icon":"https://example.com/icon.svg"} +``` + +##### POST Request +``` +POST /solana-pay/transction-request?order=12345 HTTP/1.1 +Host: example.com +Connection: close +Accept: application/json +Accept-Encoding: br, gzip, deflate +Content-Type: application/json +Content-Length: 57 + +{"account":"mvines9iiHiQTysrwkJjGf2gb9Ex9jXJX8ns3qwf2kN"} +``` + +##### POST Response +``` +HTTP/1.1 200 OK +Connection: close +Content-Type: application/json +Content-Length: 298 +Content-Encoding: gzip + +{"message":"Thanks for all the fish","transaction":"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAECC4JMKqNplIXybGb/GhK1ofdVWeuEjXnQor7gi0Y2hMcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQECAAAMAgAAAAAAAAAAAAAA"} +``` +### Sign-message Request + +#### POST Request + +The wallet must make an HTTP `POST` JSON request to the URL with a body of +```json +{"account":""} +``` + +The `` value must be the base58-encoded public key of an account that may sign the transaction. + +The wallet should make the request with an [Accept-Encoding header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding), and the application should respond with a [Content-Encoding header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding) for HTTP compression. + +The wallet should display the domain of the URL as the request is being made. If a `GET` request was made, the wallet should also display the label and render the icon image from the response. + +#### POST Response + +The wallet must handle HTTP [client error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses), [server error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses), and [redirect responses](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages). The application must respond with these, or with an HTTP `OK` JSON response with a body of +```json +{"transaction":""} +``` + +The `` value must be a base64-encoded [serialized transaction](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#serialize). The wallet must base64-decode the transaction and [deserialize it](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#from). + +The application may respond with a partially or fully signed transaction. The wallet must validate the transaction as **untrusted**. + +If the transaction [`signatures`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#signatures) are empty: + - The application should set the [`feePayer`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#feePayer) to the `account` in the request, or the zero value (`new PublicKey(0)` or `new PublicKey("11111111111111111111111111111111")`). + - The application should set the [`recentBlockhash`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#recentBlockhash) to the [latest blockhash](https://solana-labs.github.io/solana-web3.js/classes/Connection.html#getLatestBlockhash), or the zero value (`new PublicKey(0).toBase58()` or `"11111111111111111111111111111111"`). + - The wallet must ignore the [`feePayer`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#feePayer) in the transaction and set the `feePayer` to the `account` in the request. + - The wallet must ignore the [`recentBlockhash`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#recentBlockhash) in the transaction and set the `recentBlockhash` to the [latest blockhash](https://solana-labs.github.io/solana-web3.js/classes/Connection.html#getLatestBlockhash). + +If the transaction [`signatures`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#signatures) are nonempty: + - The application must set the [`feePayer`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#feePayer) to the [public key of the first signature](https://solana-labs.github.io/solana-web3.js/modules.html#SignaturePubkeyPair). + - The application must set the [`recentBlockhash`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#recentBlockhash) to the [latest blockhash](https://solana-labs.github.io/solana-web3.js/classes/Connection.html#getLatestBlockhash). + - The application must serialize and deserialize the transaction before signing it. This ensures consistent ordering of the account keys, as a workaround for [this issue](https://github.com/solana-labs/solana/issues/21722). + - The wallet must not set the [`feePayer`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#feePayer) and [`recentBlockhash`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#recentBlockhash). + - The wallet must verify the signatures, and if any are invalid, the wallet must reject the transaction as **malformed**. + +The wallet must only sign the transaction with the `account` in the request, and must do so only if a signature for the `account` in the request is expected. + +If any signature except a signature for the `account` in the request is expected, the wallet must reject the transaction as **malicious**. + +The application may also include an optional `message` field in the response body: +```json +{"message":"","transaction":""} +``` + +The `` value must be a UTF-8 string that describes the nature of the transaction response. + +For example, this might be the name of an item being purchased, a discount applied to the purchase, or a thank you note. The wallet should display the value to the user. + +The wallet and application should allow additional fields in the request body and response body, which may be added by future specification. + +### Sign-message Request Example + +##### URL describing a sign-message request. +``` +solana:https://example.com/solana-pay/sign-message ``` ##### URL describing a transaction request with query parameters. ``` -solana:https%3A%2F%2Fexample.com%2Fsolana-pay%3Forder%3D12345 +solana:https%3A%2F%2Fexample.com%2Fsolana-pay%2Fsign-message%3Fid%3D678910 ``` ##### GET Request ``` -GET /solana-pay?order=12345 HTTP/1.1 +GET /solana-pay/sign-message?id=678910 HTTP/1.1 Host: example.com Connection: close Accept: application/json @@ -222,7 +335,7 @@ Content-Encoding: gzip ##### POST Request ``` -POST /solana-pay?order=12345 HTTP/1.1 +POST /solana-pay/sign-message?id=678910 HTTP/1.1 Host: example.com Connection: close Accept: application/json From d52cf6a1d8e94cd9d3c6ebe0c5e1ce2e595e98eb Mon Sep 17 00:00:00 2001 From: Sam Hogan Date: Wed, 19 Oct 2022 09:27:32 -0600 Subject: [PATCH 09/20] Update SPEC.md --- SPEC.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/SPEC.md b/SPEC.md index 89d3aa4a..1e553f4e 100644 --- a/SPEC.md +++ b/SPEC.md @@ -101,12 +101,14 @@ A Solana Pay interactive request URL describes an interactive request where the 1. Transaction Request: A Solana Pay transaction request URL describes an interactive request that returns any Solana transaction. 2. Sign-message Request: A Solana Pay sign-message request URL describes an interactive request that is used to verify ownership of an address. + +The initial request URL structure for both types of interactive requests are the same. As such, wallets will not know which type of interaction is being requested until the POST request response payload is received from the server. + +### Link ```html solana: ``` -The initial request URL structure for both types of interactive requests are the same. As such, wallets will not know which type of interaction is being requested until the response payload is received from the server. -### Link A single `link` field is required as the pathname. The value must be a conditionally [URL-encoded](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) absolute HTTPS URL. If the URL contains query parameters, it must be URL-encoded. Protocol query parameters may be added to this specification. URL-encoding the value prevents conflicting with protocol parameters. From 4c9e9d9b3426c27e9db2e5da3617abec1cadb411 Mon Sep 17 00:00:00 2001 From: Sam Hogan Date: Wed, 19 Oct 2022 10:27:01 -0600 Subject: [PATCH 10/20] Update SPEC.md --- SPEC.md | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/SPEC.md b/SPEC.md index 1e553f4e..be340760 100644 --- a/SPEC.md +++ b/SPEC.md @@ -258,7 +258,7 @@ The wallet must make an HTTP `POST` JSON request to the URL with a body of {"account":""} ``` -The `` value must be the base58-encoded public key of an account that may sign the transaction. +The `` value must be the base58-encoded public key of an account that may sign the data. The wallet should make the request with an [Accept-Encoding header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding), and the application should respond with a [Content-Encoding header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding) for HTTP compression. @@ -268,36 +268,20 @@ The wallet should display the domain of the URL as the request is being made. If The wallet must handle HTTP [client error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses), [server error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses), and [redirect responses](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages). The application must respond with these, or with an HTTP `OK` JSON response with a body of ```json -{"transaction":""} +{"data":"","state":""} ``` -The `` value must be a base64-encoded [serialized transaction](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#serialize). The wallet must base64-decode the transaction and [deserialize it](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#from). - -The application may respond with a partially or fully signed transaction. The wallet must validate the transaction as **untrusted**. - -If the transaction [`signatures`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#signatures) are empty: - - The application should set the [`feePayer`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#feePayer) to the `account` in the request, or the zero value (`new PublicKey(0)` or `new PublicKey("11111111111111111111111111111111")`). - - The application should set the [`recentBlockhash`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#recentBlockhash) to the [latest blockhash](https://solana-labs.github.io/solana-web3.js/classes/Connection.html#getLatestBlockhash), or the zero value (`new PublicKey(0).toBase58()` or `"11111111111111111111111111111111"`). - - The wallet must ignore the [`feePayer`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#feePayer) in the transaction and set the `feePayer` to the `account` in the request. - - The wallet must ignore the [`recentBlockhash`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#recentBlockhash) in the transaction and set the `recentBlockhash` to the [latest blockhash](https://solana-labs.github.io/solana-web3.js/classes/Connection.html#getLatestBlockhash). - -If the transaction [`signatures`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#signatures) are nonempty: - - The application must set the [`feePayer`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#feePayer) to the [public key of the first signature](https://solana-labs.github.io/solana-web3.js/modules.html#SignaturePubkeyPair). - - The application must set the [`recentBlockhash`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#recentBlockhash) to the [latest blockhash](https://solana-labs.github.io/solana-web3.js/classes/Connection.html#getLatestBlockhash). - - The application must serialize and deserialize the transaction before signing it. This ensures consistent ordering of the account keys, as a workaround for [this issue](https://github.com/solana-labs/solana/issues/21722). - - The wallet must not set the [`feePayer`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#feePayer) and [`recentBlockhash`](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#recentBlockhash). - - The wallet must verify the signatures, and if any are invalid, the wallet must reject the transaction as **malformed**. +The `` value must be a [UTF-8 encoded](https://developer.mozilla.org/en-US/docs/Glossary/UTF-8) string value. The wallet must sign the `data` value with the private key that corresponds to the `account` in the request and send it back to the server from which is was retrieved. -The wallet must only sign the transaction with the `account` in the request, and must do so only if a signature for the `account` in the request is expected. +The `` value must be a UTF-8 encoded string value that functions as a MAC. The wallet will pass this value back to the server in order to verify that the contents of the field were not modified before signing. -If any signature except a signature for the `account` in the request is expected, the wallet must reject the transaction as **malicious**. The application may also include an optional `message` field in the response body: ```json -{"message":"","transaction":""} +{"message":"","data":"","state":""} ``` -The `` value must be a UTF-8 string that describes the nature of the transaction response. +The `` value must be a UTF-8 string that describes the nature of the sign-message response. For example, this might be the name of an item being purchased, a discount applied to the purchase, or a thank you note. The wallet should display the value to the user. From 85e27530aa9243c2e785aee553accd56fb4b562d Mon Sep 17 00:00:00 2001 From: Sam Hogan Date: Wed, 19 Oct 2022 10:30:10 -0600 Subject: [PATCH 11/20] Update SPEC.md --- SPEC.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SPEC.md b/SPEC.md index be340760..22c8c996 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,18 +1,18 @@ # Solana Pay Specification ## Summary -A standard protocol to encode Solana transaction requests within URLs to enable payments and other use cases. +A standard protocol to encode Solana transaction and message-signing requests within URLs to enable payments, authentication, and other use cases. Rough consensus on this spec has been reached, and implementations exist in Phantom, FTX, and Slope. This standard draws inspiration from [BIP 21](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki) and [EIP 681](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-681.md). ## Motivation -A standard URL protocol for requesting native SOL transfers, SPL Token transfers, and Solana transactions allows for a better user experience across apps and wallets in the Solana ecosystem. +A standard URL protocol for requesting native SOL transfers, SPL Token transfers, Solana transactions, and message signing allows for a better user experience across apps and wallets in the Solana ecosystem. -These URLs may be encoded in QR codes or NFC tags, or sent between users and applications to request payment and compose transactions. +These URLs may be encoded in QR codes or NFC tags, or sent between users and applications to request payment, compose transactions, and sign messages. -Applications should ensure that a transaction has been confirmed and is valid before they release goods or services being sold, or grant access to objects or events. +Applications should ensure that a transaction has been confirmed before they release goods or services being sold. Applications should also ensure that signed messages are valid before granting access to objects or events. Mobile wallets should register to handle the URL scheme to provide a seamless yet secure experience when Solana Pay URLs are encountered in the environment. From d5c6add3d7cb2c9329d9ecbadecfe631abeb3b31 Mon Sep 17 00:00:00 2001 From: Sam Hogan Date: Wed, 19 Oct 2022 10:31:50 -0600 Subject: [PATCH 12/20] Update SPEC.md --- SPEC.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPEC.md b/SPEC.md index 22c8c996..e33279dc 100644 --- a/SPEC.md +++ b/SPEC.md @@ -12,7 +12,7 @@ A standard URL protocol for requesting native SOL transfers, SPL Token transfers These URLs may be encoded in QR codes or NFC tags, or sent between users and applications to request payment, compose transactions, and sign messages. -Applications should ensure that a transaction has been confirmed before they release goods or services being sold. Applications should also ensure that signed messages are valid before granting access to objects or events. +Applications should ensure that a transaction has been confirmed, or that a signed message is valid, before they release goods or services being sold, or grant access to objects or events. Mobile wallets should register to handle the URL scheme to provide a seamless yet secure experience when Solana Pay URLs are encountered in the environment. From 8c6a3e10de3e9d379677ada27753ec0d000d871d Mon Sep 17 00:00:00 2001 From: Sam Hogan Date: Wed, 19 Oct 2022 10:57:00 -0600 Subject: [PATCH 13/20] Update SPEC.md --- SPEC.md | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/SPEC.md b/SPEC.md index e33279dc..fbc2c69f 100644 --- a/SPEC.md +++ b/SPEC.md @@ -271,9 +271,9 @@ The wallet must handle HTTP [client error](https://developer.mozilla.org/en-US/d {"data":"","state":""} ``` -The `` value must be a [UTF-8 encoded](https://developer.mozilla.org/en-US/docs/Glossary/UTF-8) string value. The wallet must sign the `data` value with the private key that corresponds to the `account` in the request and send it back to the server from which is was retrieved. +The `` value must be a [UTF-8 encoded](https://developer.mozilla.org/en-US/docs/Glossary/UTF-8) string value. The wallet must sign the `data` value with the private key that corresponds to the `account` in the request and send it back to the server in a [PUT request](https://github.com/bedrock-foundation/solana-pay/edit/master/SPEC.md#put-request). -The `` value must be a UTF-8 encoded string value that functions as a MAC. The wallet will pass this value back to the server in order to verify that the contents of the field were not modified before signing. +The `` value must be a UTF-8 encoded string value that functions as a MAC. The wallet will pass this value back to the server in order to verify that the contents of the `` field were not modified before signing. The application may also include an optional `message` field in the response body: @@ -283,7 +283,33 @@ The application may also include an optional `message` field in the response bod The `` value must be a UTF-8 string that describes the nature of the sign-message response. -For example, this might be the name of an item being purchased, a discount applied to the purchase, or a thank you note. The wallet should display the value to the user. +For example, this might be the name of the application or event with which the user is interacting, context about how the sign-message request is being used, or a thank you note. The wallet should display the value to the user. + +The wallet and application should allow additional fields in the request body and response body, which may be added by future specification. + +#### PUT Request + +The wallet must make an HTTP `PUT` JSON request to the URL with a body of +```json +{"account":"","state":"","signature":""} +``` + +The `` value must be the base58-encoded public key of an account that may sign the data. +The `` value must be the unmodifed UTF-8-encoded `` value from the response of the preceeding POST request. +The `` The value is the base-58 encoded signature from signing the field with the users private key. + +The wallet should make the request with an [Accept-Encoding header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding), and the application should respond with a [Content-Encoding header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding) for HTTP compression. + +The wallet should display the domain of the URL as the request is being made. If a `GET` request was made, the wallet should also display the label and render the icon image from the response. + +#### PUT Response + +The wallet must handle HTTP [client error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses), [server error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses), and [redirect responses](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages). The application must respond with these, or with an HTTP `OK` JSON response with a body of +```json +{"success":""} +``` + +The `` value must be a boolean value indicating whether signature verification succeeded or failed. The wallet and application should allow additional fields in the request body and response body, which may be added by future specification. From 56b1ab6f723b2c1c671712be2babf249c07cbe32 Mon Sep 17 00:00:00 2001 From: Sam Hogan Date: Wed, 19 Oct 2022 11:01:05 -0600 Subject: [PATCH 14/20] Update SPEC.md --- SPEC.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SPEC.md b/SPEC.md index fbc2c69f..dc2a75e5 100644 --- a/SPEC.md +++ b/SPEC.md @@ -271,9 +271,9 @@ The wallet must handle HTTP [client error](https://developer.mozilla.org/en-US/d {"data":"","state":""} ``` -The `` value must be a [UTF-8 encoded](https://developer.mozilla.org/en-US/docs/Glossary/UTF-8) string value. The wallet must sign the `data` value with the private key that corresponds to the `account` in the request and send it back to the server in a [PUT request](https://github.com/bedrock-foundation/solana-pay/edit/master/SPEC.md#put-request). +The `` value must be a [UTF-8 encoded](https://developer.mozilla.org/en-US/docs/Glossary/UTF-8) string value. The wallet must sign the `data` value with the private key that corresponds to the `account` in the request and send the resulting signature back to the server in the proceeding [PUT request](https://github.com/bedrock-foundation/solana-pay/edit/master/SPEC.md#put-request). -The `` value must be a UTF-8 encoded string value that functions as a MAC. The wallet will pass this value back to the server in order to verify that the contents of the `` field were not modified before signing. +The `` value must be a UTF-8 encoded string value that functions as a MAC. The wallet will pass this value back to the server in the [PUT request](https://github.com/bedrock-foundation/solana-pay/edit/master/SPEC.md#put-request) in order to verify that the contents of the `` field were not modified before signing. The application may also include an optional `message` field in the response body: From 421a2ac2096c2feea366514b8239044df9d843e3 Mon Sep 17 00:00:00 2001 From: Sam Hogan Date: Wed, 19 Oct 2022 11:41:36 -0600 Subject: [PATCH 15/20] Update SPEC.md --- SPEC.md | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/SPEC.md b/SPEC.md index dc2a75e5..5b2ed3c7 100644 --- a/SPEC.md +++ b/SPEC.md @@ -102,7 +102,7 @@ A Solana Pay interactive request URL describes an interactive request where the 1. Transaction Request: A Solana Pay transaction request URL describes an interactive request that returns any Solana transaction. 2. Sign-message Request: A Solana Pay sign-message request URL describes an interactive request that is used to verify ownership of an address. -The initial request URL structure for both types of interactive requests are the same. As such, wallets will not know which type of interaction is being requested until the POST request response payload is received from the server. +The request URL structure for both types of interactive requests are the same. As such, wallets will not know which type of interaction is being requested until the POST request response payload is received from the server. ### Link ```html @@ -258,7 +258,7 @@ The wallet must make an HTTP `POST` JSON request to the URL with a body of {"account":""} ``` -The `` value must be the base58-encoded public key of an account that may sign the data. +The `` value must be the base58-encoded public key of the account that will sign the message. The wallet should make the request with an [Accept-Encoding header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding), and the application should respond with a [Content-Encoding header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding) for HTTP compression. @@ -273,7 +273,7 @@ The wallet must handle HTTP [client error](https://developer.mozilla.org/en-US/d The `` value must be a [UTF-8 encoded](https://developer.mozilla.org/en-US/docs/Glossary/UTF-8) string value. The wallet must sign the `data` value with the private key that corresponds to the `account` in the request and send the resulting signature back to the server in the proceeding [PUT request](https://github.com/bedrock-foundation/solana-pay/edit/master/SPEC.md#put-request). -The `` value must be a UTF-8 encoded string value that functions as a MAC. The wallet will pass this value back to the server in the [PUT request](https://github.com/bedrock-foundation/solana-pay/edit/master/SPEC.md#put-request) in order to verify that the contents of the `` field were not modified before signing. +The `` value must be a UTF-8 encoded string value that functions as a MAC. The wallet will pass this value back to the server in the [PUT request](https://github.com/bedrock-foundation/solana-pay/edit/master/SPEC.md#put-request) in order to verify that the contents of the `` field were not modified. The application may also include an optional `message` field in the response body: @@ -289,14 +289,14 @@ The wallet and application should allow additional fields in the request body an #### PUT Request -The wallet must make an HTTP `PUT` JSON request to the URL with a body of +The PUT request is used to send the results of signing the message back to the server. The wallet must make an HTTP `PUT` JSON request to the URL with a body of ```json {"account":"","state":"","signature":""} ``` -The `` value must be the base58-encoded public key of an account that may sign the data. +The `` value must be the base58-encoded public key of the account that signed the message. The `` value must be the unmodifed UTF-8-encoded `` value from the response of the preceeding POST request. -The `` The value is the base-58 encoded signature from signing the field with the users private key. +The `` value is the base-58 encoded signature from signing the `` field with the users private key. The wallet should make the request with an [Accept-Encoding header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding), and the application should respond with a [Content-Encoding header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding) for HTTP compression. @@ -320,7 +320,7 @@ The wallet and application should allow additional fields in the request body an solana:https://example.com/solana-pay/sign-message ``` -##### URL describing a transaction request with query parameters. +##### URL describing a sign-message request with query parameters. ``` solana:https%3A%2F%2Fexample.com%2Fsolana-pay%2Fsign-message%3Fid%3D678910 ``` @@ -366,7 +366,31 @@ Content-Type: application/json Content-Length: 298 Content-Encoding: gzip -{"message":"Thanks for all the fish","transaction":"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAECC4JMKqNplIXybGb/GhK1ofdVWeuEjXnQor7gi0Y2hMcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQECAAAMAgAAAAAAAAAAAAAA"} +{"message":"Sign the message to login","data":"alskdfjaisdjfasjdflkasdjfiaj","state":"statehere"} +``` + +##### PUT Request +``` +POST /solana-pay/sign-message?id=678910 HTTP/1.1 +Host: example.com +Connection: close +Accept: application/json +Accept-Encoding: br, gzip, deflate +Content-Type: application/json +Content-Length: 57 + +{"account":"mvines9iiHiQTysrwkJjGf2gb9Ex9jXJX8ns3qwf2kN", "signature":"signature here","state":"statehere"} +``` + +##### PUT Response +``` +HTTP/1.1 200 OK +Connection: close +Content-Type: application/json +Content-Length: 298 +Content-Encoding: gzip + +{"success":true} ``` ## Extensions From bd52a5013585793b1e46bff4fa3880e9818b3270 Mon Sep 17 00:00:00 2001 From: Sam Hogan Date: Wed, 19 Oct 2022 11:50:48 -0600 Subject: [PATCH 16/20] Update SPEC.md --- SPEC.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SPEC.md b/SPEC.md index 5b2ed3c7..bbd2d00a 100644 --- a/SPEC.md +++ b/SPEC.md @@ -295,7 +295,9 @@ The PUT request is used to send the results of signing the message back to the s ``` The `` value must be the base58-encoded public key of the account that signed the message. + The `` value must be the unmodifed UTF-8-encoded `` value from the response of the preceeding POST request. + The `` value is the base-58 encoded signature from signing the `` field with the users private key. The wallet should make the request with an [Accept-Encoding header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding), and the application should respond with a [Content-Encoding header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding) for HTTP compression. From feceef41ffce99cbc5caf5b786cb3756694b1d16 Mon Sep 17 00:00:00 2001 From: Sam Heutmaker Date: Wed, 19 Oct 2022 11:51:33 -0600 Subject: [PATCH 17/20] sendSignedData -> sendSignature, other clean up --- core/src/fetchInteraction.ts | 6 +--- core/src/index.ts | 2 ++ core/src/sendSignature.ts | 54 ++++++++++++++++++++++++++++ core/src/sendSignedData.ts | 70 ------------------------------------ 4 files changed, 57 insertions(+), 75 deletions(-) create mode 100644 core/src/sendSignature.ts delete mode 100644 core/src/sendSignedData.ts diff --git a/core/src/fetchInteraction.ts b/core/src/fetchInteraction.ts index 9ef26515..14e745e7 100644 --- a/core/src/fetchInteraction.ts +++ b/core/src/fetchInteraction.ts @@ -50,10 +50,6 @@ export const isErrorResponse = (value: FetchInteractionErrorResponse): value is return !isSignMessageResponse(value) && !isTransactionResponse(value); }; -const isBase64 = (value: string): boolean => { - return /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/.test(value); -}; - /** * Fetch a transaction from a Solana Pay transaction request link. * @@ -135,8 +131,8 @@ export async function fetchInteraction( */ if (json.data) { + if (typeof json.data !== 'string') throw new FetchInteractionError('invalid data field'); if (typeof json.state !== 'string') throw new FetchInteractionError('invalid state field'); - if (!isBase64(json.data)) throw new FetchInteractionError('invalid data field'); return { data: json.data, diff --git a/core/src/index.ts b/core/src/index.ts index cac676be..a92b175d 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -2,8 +2,10 @@ export * from './constants.js'; export * from './createQR.js'; export * from './createTransfer.js'; export * from './encodeURL.js'; +export * from './fetchInteraction.js'; export * from './fetchTransaction.js'; export * from './findReference.js'; export * from './parseURL.js'; +export * from './sendSignature.js'; export * from './types.js'; export * from './validateTransfer.js'; diff --git a/core/src/sendSignature.ts b/core/src/sendSignature.ts new file mode 100644 index 00000000..647ef557 --- /dev/null +++ b/core/src/sendSignature.ts @@ -0,0 +1,54 @@ +import type { PublicKey } from '@solana/web3.js'; +import fetch from 'cross-fetch'; + +/** + * Thrown when response is invalid + */ +export class SendSignatureError extends Error { + name = 'SendSignatureError'; +} + +export type SendSignatureResponse = { + success?: boolean; +}; + +/** + * Send the results of a Solana Pay sign-message request to the server. + * + * @param account - Account that signed the data + * @param signature - The signature from signing the data. + * @param state - MAC value that was sent by the server during the POST request. + * @param link - `link` in the [Solana Pay spec](https://github.com/solana-labs/solana-pay/blob/master/SPEC.md#link). + * + * @throws {SendSignatureError} + */ +export async function sendSignature( + account: PublicKey, + state: string, + signature: string, + link: string | URL +): Promise { + const response = await fetch(String(link), { + method: 'PUT', + mode: 'cors', + cache: 'no-cache', + credentials: 'omit', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + account, + signature, + state, + }), + }); + + const json: SendSignatureResponse = await response.json(); + + if (typeof json.success !== 'boolean') { + throw new SendSignatureError('invalid response'); + } + + return json.success; +} diff --git a/core/src/sendSignedData.ts b/core/src/sendSignedData.ts deleted file mode 100644 index 28ee3784..00000000 --- a/core/src/sendSignedData.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { Commitment, Connection, PublicKey } from '@solana/web3.js'; -import type { Transaction } from '@solana/web3.js'; -import fetch from 'cross-fetch'; - -/** - * Thrown when response is invalid - */ -export class SendSignedDataError extends Error { - name = 'SendSignedDataError'; -} - -export interface FetchTransactionResponse { - transaction: Transaction; - message?: string; -} - -export interface FetchSignMessageResponse { - data: string; - state: string; - message?: string; -} - -export interface SendSignedDataErrorResponse { - message?: string; -} - -export type SendSignedDataResponse = { - success?: boolean; -}; - -/** - * Fetch a transaction from a Solana Pay transaction request link. - * - * @param account - Account that signed the data - * @param state - MAC value that was returned from the server during the the inital request. - * @param signature - The signature from signing the data. - * @param link - `link` in the [Solana Pay spec](https://github.com/solana-labs/solana-pay/blob/master/SPEC.md#link). - * - * @throws {SendSignedDataError} - */ -export async function fetchInteraction( - account: PublicKey, - state: string, - signature: string, - link: string | URL -): Promise { - const response = await fetch(String(link), { - method: 'POST', - mode: 'cors', - cache: 'no-cache', - credentials: 'omit', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - account, - state, - signature, - }), - }); - - const json: SendSignedDataResponse = await response.json(); - - if (typeof json.success === 'undefined') { - throw new SendSignedDataError('invalid response'); - } - - return json; -} From a2a8edc5fd2be7ff0d51aaa0ddad954ccbe277f5 Mon Sep 17 00:00:00 2001 From: Sam Hogan Date: Wed, 19 Oct 2022 12:17:18 -0600 Subject: [PATCH 18/20] Update SPEC.md --- SPEC.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SPEC.md b/SPEC.md index bbd2d00a..78e63bbc 100644 --- a/SPEC.md +++ b/SPEC.md @@ -368,7 +368,7 @@ Content-Type: application/json Content-Length: 298 Content-Encoding: gzip -{"message":"Sign the message to login","data":"alskdfjaisdjfasjdflkasdjfiaj","state":"statehere"} +{"message":"Sign the message to login","data":"SIGN_THIS_MESSAGE","state":"eyJhbGciOiJIUzI1NiJ9.U0lHTl9USElTX01FU1NBR0U.KcZ1FnrT1ImAL-7LbALfZOx9F4I4LMuEE8_bg5Zmec4"} ``` ##### PUT Request @@ -381,7 +381,7 @@ Accept-Encoding: br, gzip, deflate Content-Type: application/json Content-Length: 57 -{"account":"mvines9iiHiQTysrwkJjGf2gb9Ex9jXJX8ns3qwf2kN", "signature":"signature here","state":"statehere"} +{"account":"mvines9iiHiQTysrwkJjGf2gb9Ex9jXJX8ns3qwf2kN", "signature":"3ApozYFyp2ZxWuGvJS7Q1oV8M3YsLMV3WmwbjGCgktqXfdevjCZ92vA4F9V7Xj7KrN7JTtYStBSBeWnNN7vyHkg5","state":"eyJhbGciOiJIUzI1NiJ9.U0lHTl9USElTX01FU1NBR0U.KcZ1FnrT1ImAL-7LbALfZOx9F4I4LMuEE8_bg5Zmec4"} ``` ##### PUT Response From fd3c95c5dbaccac1311a185fb51e1f91f9e17034 Mon Sep 17 00:00:00 2001 From: Sam Hogan Date: Wed, 19 Oct 2022 12:17:57 -0600 Subject: [PATCH 19/20] Update SPEC.md --- SPEC.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPEC.md b/SPEC.md index 78e63bbc..ef7ed8b6 100644 --- a/SPEC.md +++ b/SPEC.md @@ -381,7 +381,7 @@ Accept-Encoding: br, gzip, deflate Content-Type: application/json Content-Length: 57 -{"account":"mvines9iiHiQTysrwkJjGf2gb9Ex9jXJX8ns3qwf2kN", "signature":"3ApozYFyp2ZxWuGvJS7Q1oV8M3YsLMV3WmwbjGCgktqXfdevjCZ92vA4F9V7Xj7KrN7JTtYStBSBeWnNN7vyHkg5","state":"eyJhbGciOiJIUzI1NiJ9.U0lHTl9USElTX01FU1NBR0U.KcZ1FnrT1ImAL-7LbALfZOx9F4I4LMuEE8_bg5Zmec4"} +{"account":"mvines9iiHiQTysrwkJjGf2gb9Ex9jXJX8ns3qwf2kN","signature":"3ApozYFyp2ZxWuGvJS7Q1oV8M3YsLMV3WmwbjGCgktqXfdevjCZ92vA4F9V7Xj7KrN7JTtYStBSBeWnNN7vyHkg5","state":"eyJhbGciOiJIUzI1NiJ9.U0lHTl9USElTX01FU1NBR0U.KcZ1FnrT1ImAL-7LbALfZOx9F4I4LMuEE8_bg5Zmec4"} ``` ##### PUT Response From c0894938c3c515b8345c81005fe567903230d4b0 Mon Sep 17 00:00:00 2001 From: Sam Hogan Date: Wed, 19 Oct 2022 12:23:04 -0600 Subject: [PATCH 20/20] Update SPEC.md --- SPEC.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SPEC.md b/SPEC.md index ef7ed8b6..285969f9 100644 --- a/SPEC.md +++ b/SPEC.md @@ -271,9 +271,9 @@ The wallet must handle HTTP [client error](https://developer.mozilla.org/en-US/d {"data":"","state":""} ``` -The `` value must be a [UTF-8 encoded](https://developer.mozilla.org/en-US/docs/Glossary/UTF-8) string value. The wallet must sign the `data` value with the private key that corresponds to the `account` in the request and send the resulting signature back to the server in the proceeding [PUT request](https://github.com/bedrock-foundation/solana-pay/edit/master/SPEC.md#put-request). +The `` value must be a [UTF-8](https://developer.mozilla.org/en-US/docs/Glossary/UTF-8) string value. The wallet must sign the `data` value with the private key that corresponds to the `account` in the request and send the resulting signature back to the server in the proceeding [PUT request](https://github.com/bedrock-foundation/solana-pay/edit/master/SPEC.md#put-request). -The `` value must be a UTF-8 encoded string value that functions as a MAC. The wallet will pass this value back to the server in the [PUT request](https://github.com/bedrock-foundation/solana-pay/edit/master/SPEC.md#put-request) in order to verify that the contents of the `` field were not modified. +The `` value must be a [UTF-8](https://developer.mozilla.org/en-US/docs/Glossary/UTF-8) string value that functions as a [MAC](https://en.wikipedia.org/wiki/Message_authentication_code). The wallet will pass this value back to the server in the [PUT request](https://github.com/bedrock-foundation/solana-pay/edit/master/SPEC.md#put-request) in order to verify that the contents of the `` field were not modified. The application may also include an optional `message` field in the response body: @@ -281,7 +281,7 @@ The application may also include an optional `message` field in the response bod {"message":"","data":"","state":""} ``` -The `` value must be a UTF-8 string that describes the nature of the sign-message response. +The `` value must be a UTF-8 encoded string that describes the nature of the sign-message response. For example, this might be the name of the application or event with which the user is interacting, context about how the sign-message request is being used, or a thank you note. The wallet should display the value to the user. @@ -296,9 +296,9 @@ The PUT request is used to send the results of signing the message back to the s The `` value must be the base58-encoded public key of the account that signed the message. -The `` value must be the unmodifed UTF-8-encoded `` value from the response of the preceeding POST request. +The `` value must be the unmodifed `` value from the response of the preceeding POST request. -The `` value is the base-58 encoded signature from signing the `` field with the users private key. +The `` value is the base58-encoded signature from signing the `` field with the users private key. The wallet should make the request with an [Accept-Encoding header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding), and the application should respond with a [Content-Encoding header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding) for HTTP compression.