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

Sign Auth Entry API & modal flow #949

Merged
merged 21 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6b98f9a
adds new path for signing auth entries, adds SignAuthEntry component …
aristidesstaffieri Aug 21, 2023
a6bad1e
Adds basic signing component for auth entry sign flow
aristidesstaffieri Aug 22, 2023
5af3394
Adds new external api signAuthEntry, adds docs, adds sign auth entry …
aristidesstaffieri Aug 22, 2023
380530a
Added translations
aristidesstaffieri Aug 22, 2023
a06003e
adds buildInvocationTree to render formatted json for invocations and…
aristidesstaffieri Aug 23, 2023
c589044
use correct key in responseQueue handler for signAuthEntry, clean up …
aristidesstaffieri Aug 23, 2023
db78b9f
splits SignTx and SignBlob, makes new route for sign-blob
aristidesstaffieri Aug 23, 2023
7f7eb56
use buildInvocationTree in transaction render for signTx
aristidesstaffieri Aug 24, 2023
3fb4eff
Added translations
aristidesstaffieri Aug 24, 2023
4432d60
Added translations
aristidesstaffieri Aug 24, 2023
2efbb74
remove accidental tarball commit
aristidesstaffieri Aug 24, 2023
63a77ea
adds missing loading states, fixes missing error handling in message …
aristidesstaffieri Aug 24, 2023
4b0d05a
remove extra line
aristidesstaffieri Aug 24, 2023
47ea4f9
refactors signing flows to use a common helper hook
aristidesstaffieri Aug 25, 2023
433a42d
dont allow HW signing on sign blob or sign auth entry, not currently …
aristidesstaffieri Aug 25, 2023
1fdbabf
Added translations
aristidesstaffieri Aug 25, 2023
d0b7bdf
Added translations
aristidesstaffieri Aug 25, 2023
d1f4093
updates HW wallet not supported text
aristidesstaffieri Aug 25, 2023
860e293
Added translations
aristidesstaffieri Aug 25, 2023
e03372a
refactors SorobanContext to accoutn for missing rpc urls
aristidesstaffieri Aug 28, 2023
06295ca
moves dispatch to be in shared signing hook
aristidesstaffieri Aug 28, 2023
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
29 changes: 29 additions & 0 deletions @shared/api/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const submitTransaction = async (
});
} catch (e) {
console.error(e);
throw e;
}
const { signedTransaction, error } = response;

Expand All @@ -86,6 +87,7 @@ export const submitBlob = async (
});
} catch (e) {
console.error(e);
throw e;
}
const { signedBlob, error } = response;

Expand All @@ -95,6 +97,33 @@ export const submitBlob = async (
return signedBlob;
};

export const submitAuthEntry = async (
entryXdr: string,
opts?: {
accountToSign?: string;
},
): Promise<string> => {
let response = { signedAuthEntry: "", error: "" };
const _opts = opts || {};
const accountToSign = _opts.accountToSign || "";
try {
response = await sendMessageToContentScript({
entryXdr,
accountToSign,
type: EXTERNAL_SERVICE_TYPES.SUBMIT_AUTH_ENTRY,
});
} catch (e) {
console.error(e);
Copy link

Choose a reason for hiding this comment

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

should this also do response.error = e;?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it's more appropriate to throw here because that would be an implementation error when the other is a type or API error. Added in 63a77ea but let me know if you disagree.

Copy link

Choose a reason for hiding this comment

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

def up to y'all cuz I'm unfamiliar w/ your error-handling conventions, I just more wanted to call out the distinction between exceptions and the response.error field

}
const { signedAuthEntry, error } = response;

if (error) {
throw error;
throw error;
Copy link
Contributor

Choose a reason for hiding this comment

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

repeated line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

whops, thanks for the catch. Fixed in 4b0d05a

}
return signedAuthEntry;
};

export const requestNetwork = async (): Promise<string> => {
let response = { network: "", error: "" };
try {
Expand Down
10 changes: 10 additions & 0 deletions @shared/api/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,16 @@ export const signBlob = async (): Promise<void> => {
}
};

export const signAuthEntry = async (): Promise<void> => {
try {
await sendMessageToBackground({
type: SERVICE_TYPES.SIGN_AUTH_ENTRY,
});
} catch (e) {
console.error(e);
}
};

export const signFreighterTransaction = async ({
transactionXDR,
network,
Expand Down
10 changes: 9 additions & 1 deletion @shared/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface Response {
transactionXDR: string;
signedTransaction: string;
signedBlob: string;
signedAuthEntry: string;
source: string;
type: SERVICE_TYPES;
url: string;
Expand Down Expand Up @@ -96,7 +97,14 @@ export interface ExternalRequestBlob extends ExternalRequestBase {
blob: string;
}

export type ExternalRequest = ExternalRequestTx | ExternalRequestBlob;
export interface ExternalRequestAuthEntry extends ExternalRequestBase {
entryXdr: string;
}

export type ExternalRequest =
| ExternalRequestTx
| ExternalRequestBlob
| ExternalRequestAuthEntry;

export interface Account {
publicKey: string;
Expand Down
2 changes: 2 additions & 0 deletions @shared/constants/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export enum SERVICE_TYPES {
GRANT_ACCESS = "GRANT_ACCESS",
SIGN_TRANSACTION = "SIGN_TRANSACTION",
SIGN_BLOB = "SIGN_BLOB",
SIGN_AUTH_ENTRY = "SIGN_AUTH_ENTRY",
HANDLE_SIGNED_HW_TRANSACTION = "HANDLE_SIGNED_HW_TRANSACTION",
REJECT_TRANSACTION = "REJECT_TRANSACTION",
SIGN_FREIGHTER_TRANSACTION = "SIGN_FREIGHTER_TRANSACTION",
Expand Down Expand Up @@ -45,6 +46,7 @@ export enum EXTERNAL_SERVICE_TYPES {
REQUEST_ACCESS = "REQUEST_ACCESS",
SUBMIT_TRANSACTION = "SUBMIT_TRANSACTION",
SUBMIT_BLOB = "SUBMIT_BLOB",
SUBMIT_AUTH_ENTRY = "SUBMIT_AUTH_ENTRY",
REQUEST_NETWORK = "REQUEST_NETWORK",
REQUEST_NETWORK_DETAILS = "REQUEST_NETWORK_DETAILS",
REQUEST_CONNECTION_STATUS = "REQUEST_CONNECTION_STATUS",
Expand Down
1 change: 1 addition & 0 deletions @stellar/freighter-api/src/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ describe("freighter API", () => {
expect(typeof FreighterAPI.getPublicKey).toBe("function");
expect(typeof FreighterAPI.signTransaction).toBe("function");
expect(typeof FreighterAPI.signBlob).toBe("function");
expect(typeof FreighterAPI.signAuthEntry).toBe("function");
});
});
1 change: 1 addition & 0 deletions @stellar/freighter-api/src/__tests__/signBlob.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe("signBlob", () => {
const blob = await signBlob();
expect(blob).toBe(TEST_BLOB);
});

it("throws a generic error", () => {
const TEST_ERROR = "Error!";
apiExternal.submitBlob = jest.fn().mockImplementation(() => {
Expand Down
3 changes: 3 additions & 0 deletions @stellar/freighter-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getPublicKey } from "./getPublicKey";
import { signTransaction } from "./signTransaction";
import { signBlob } from "./signBlob";
import { signAuthEntry } from "./signAuthEntry";
import { isConnected } from "./isConnected";
import { getNetwork } from "./getNetwork";
import { getNetworkDetails } from "./getNetworkDetails";
Expand All @@ -14,6 +15,7 @@ export {
getPublicKey,
signTransaction,
signBlob,
signAuthEntry,
isConnected,
getNetwork,
getNetworkDetails,
Expand All @@ -25,6 +27,7 @@ export default {
getPublicKey,
signTransaction,
signBlob,
signAuthEntry,
isConnected,
getNetwork,
getNetworkDetails,
Expand Down
10 changes: 10 additions & 0 deletions @stellar/freighter-api/src/signAuthEntry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { submitAuthEntry } from "@shared/api/external";
import { isBrowser } from ".";

export const signAuthEntry = (
entryXdr: string,
opts?: {
accountToSign?: string;
}
): Promise<string> =>
isBrowser ? submitAuthEntry(entryXdr, opts) : Promise.resolve("");
6 changes: 5 additions & 1 deletion docs/docs/guide/usingFreighterNode.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ or import just the modules you require:
import {
isConnected,
getPublicKey,
signAuthEntry,
signTransaction,
signBlob,
} from "@stellar/freighter-api";
Expand Down Expand Up @@ -82,6 +83,7 @@ If the user has authorized your application previously, it will be on the extens
import {
isConnected,
getPublicKey,
signAuthEntry,
signTransaction,
signBlob,
} from "@stellar/freighter-api";
Expand Down Expand Up @@ -124,6 +126,7 @@ import {
isAllowed,
setAllowed,
getUserInfo,
signAuthEntry,
signTransaction,
signBlob,
} from "@stellar/freighter-api";
Expand Down Expand Up @@ -177,6 +180,7 @@ This function is useful for determining what network the user has configured Fre
import {
isConnected,
getNetwork,
signAuthEntry,
signTransaction,
signBlob,
} from "@stellar/freighter-api";
Expand Down Expand Up @@ -234,7 +238,7 @@ import {
isConnected,
getPublicKey,
signTransaction,
signBlob
signBlob,
} from "@stellar/freighter-api";

if (await isConnected()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import browser from "webextension-polyfill";
import { Store } from "redux";

import {
ExternalRequestAuthEntry,
ExternalRequestBlob,
ExternalRequestTx,
ExternalRequest as Request,
Expand Down Expand Up @@ -39,6 +40,7 @@ import {
import { publicKeySelector } from "background/ducks/session";

import {
authEntryQueue,
blobQueue,
responseQueue,
transactionQueue,
Expand Down Expand Up @@ -254,9 +256,7 @@ export const freighterApiMessageListener = (
blobQueue.push(blobData);
const encodedBlob = encodeObject(blobData);
const popup = browser.windows.create({
url: chrome.runtime.getURL(
`/index.html#/sign-transaction?${encodedBlob}`,
),
url: chrome.runtime.getURL(`/index.html#/sign-blob?${encodedBlob}`),
...WINDOW_SETTINGS,
});

Expand Down Expand Up @@ -286,6 +286,59 @@ export const freighterApiMessageListener = (
});
};

const submitAuthEntry = async () => {
const { entryXdr, accountToSign } = request as ExternalRequestAuthEntry;

const { tab, url: tabUrl = "" } = sender;
const domain = getUrlHostname(tabUrl);
const punycodedDomain = getPunycodedDomain(domain);

const allowListStr = (await localStore.getItem(ALLOWLIST_ID)) || "";
const allowList = allowListStr.split(",");
const isDomainListedAllowed = await isSenderAllowed({ sender });

const authEntry = {
entry: entryXdr,
accountToSign,
tab,
url: tabUrl,
};

authEntryQueue.push(authEntry);
const encodedAuthEntry = encodeObject(authEntry);
const popup = browser.windows.create({
url: chrome.runtime.getURL(
`/index.html#/sign-auth-entry?${encodedAuthEntry}`,
),
...WINDOW_SETTINGS,
});

return new Promise((resolve) => {
if (!popup) {
resolve({ error: "Couldn't open access prompt" });
} else {
browser.windows.onRemoved.addListener(() =>
resolve({
error: "User declined access",
}),
);
}
const response = (signedAuthEntry: string) => {
if (signedAuthEntry) {
if (!isDomainListedAllowed) {
allowList.push(punycodedDomain);
localStore.setItem(ALLOWLIST_ID, allowList.join());
}
resolve({ signedAuthEntry });
}

resolve({ error: "User declined access" });
};

responseQueue.push(response);
});
};

const requestNetwork = async () => {
let network = "";

Expand Down Expand Up @@ -378,6 +431,7 @@ export const freighterApiMessageListener = (
[EXTERNAL_SERVICE_TYPES.REQUEST_ACCESS]: requestAccess,
[EXTERNAL_SERVICE_TYPES.SUBMIT_TRANSACTION]: submitTransaction,
[EXTERNAL_SERVICE_TYPES.SUBMIT_BLOB]: submitBlob,
[EXTERNAL_SERVICE_TYPES.SUBMIT_AUTH_ENTRY]: submitAuthEntry,
[EXTERNAL_SERVICE_TYPES.REQUEST_NETWORK]: requestNetwork,
[EXTERNAL_SERVICE_TYPES.REQUEST_NETWORK_DETAILS]: requestNetworkDetails,
[EXTERNAL_SERVICE_TYPES.REQUEST_CONNECTION_STATUS]: requestConnectionStatus,
Expand Down
31 changes: 31 additions & 0 deletions extension/src/background/messageListener/popupMessageListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ export const blobQueue: Array<{
accountToSign: string;
}> = [];

export const authEntryQueue: Array<{
accountToSign: string;
tab: browser.Tabs.Tab | undefined;
entry: string; // xdr.SorobanAuthorizationEntry
url: string;
}> = [];

interface KeyPair {
publicKey: string;
privateKey: string;
Expand Down Expand Up @@ -932,6 +939,29 @@ export const popupMessageListener = (request: Request, sessionStore: Store) => {
return { error: "Session timed out" };
};

const signAuthEntry = async () => {
const privateKey = privateKeySelector(sessionStore.getState());

if (privateKey.length) {
const sourceKeys = SorobanSdk.Keypair.fromSecret(privateKey);

const authEntry = authEntryQueue.pop();

const response = authEntry
? await sourceKeys.sign(Buffer.from(authEntry.entry))
: null;

const entryResponse = responseQueue.pop();

if (typeof entryResponse === "function") {
entryResponse(response);
return {};
}
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
}

return { error: "Session timed out" };
};

const rejectTransaction = () => {
transactionQueue.pop();
const response = responseQueue.pop();
Expand Down Expand Up @@ -1224,6 +1254,7 @@ export const popupMessageListener = (request: Request, sessionStore: Store) => {
[SERVICE_TYPES.REJECT_ACCESS]: rejectAccess,
[SERVICE_TYPES.SIGN_TRANSACTION]: signTransaction,
[SERVICE_TYPES.SIGN_BLOB]: signBlob,
[SERVICE_TYPES.SIGN_AUTH_ENTRY]: signAuthEntry,
[SERVICE_TYPES.HANDLE_SIGNED_HW_TRANSACTION]: handleSignedHwTransaction,
[SERVICE_TYPES.REJECT_TRANSACTION]: rejectTransaction,
[SERVICE_TYPES.SIGN_FREIGHTER_TRANSACTION]: signFreighterTransaction,
Expand Down
4 changes: 2 additions & 2 deletions extension/src/helpers/__tests__/stellar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe("getTransactionInfo", () => {
tab: {},
});
const info = getTransactionInfo("foo");
if (!("blob" in info)) {
if (!("blob" in info) && !("entry" in info)) {
expect(info.isHttpsDomain).toBe(true);
}
});
Expand All @@ -41,7 +41,7 @@ describe("getTransactionInfo", () => {
tab: {},
});
const info = getTransactionInfo("foo");
if (!("blob" in info)) {
if (!("blob" in info) && !("entry" in info)) {
expect(info.isHttpsDomain).toBe(false);
}
});
Expand Down
7 changes: 2 additions & 5 deletions extension/src/helpers/stellar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
NetworkDetails,
} from "@shared/constants/stellar";

import { TransactionInfo } from "types/transactions";
import { parsedSearchParam, getUrlHostname } from "./urls";

// .isBigNumber() not catching correctly, so checking .isBigNumber
Expand All @@ -34,11 +35,7 @@ export const truncatedFedAddress = (addr: string) => {
export const truncatedPoolId = (poolId: string) => truncateString(poolId);

export const getTransactionInfo = (search: string) => {
const searchParams = parsedSearchParam(search);

if ("blob" in searchParams) {
return searchParams;
}
const searchParams = parsedSearchParam(search) as TransactionInfo;

const {
accountToSign,
Expand Down
Loading
Loading