Skip to content

Commit

Permalink
adds new api, signBlob
Browse files Browse the repository at this point in the history
  • Loading branch information
aristidesstaffieri committed Jul 28, 2023
1 parent cddc615 commit f3865d3
Show file tree
Hide file tree
Showing 12 changed files with 232 additions and 91 deletions.
30 changes: 30 additions & 0 deletions @shared/api/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,36 @@ export const submitTransaction = async (
return signedTransaction;
};

export const submitBlob = async (
blob: string,
opts:
| {
network: string
accountToSign: string
networkPassphrase: string
},
): Promise<string> => {
let response = { signedTransaction: "", error: "" };
const {network, networkPassphrase, accountToSign} = opts
try {
response = await sendMessageToContentScript({
transactionXDR: blob,
network,
networkPassphrase,
accountToSign,
type: EXTERNAL_SERVICE_TYPES.SUBMIT_BLOB,
});
} catch (e) {
console.error(e);
}
const { signedTransaction, error } = response;

if (error) {
throw error;
}
return signedTransaction;
}

export const requestNetwork = async (): Promise<string> => {
let response = { network: "", error: "" };
try {
Expand Down
1 change: 1 addition & 0 deletions @shared/constants/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export enum SERVICE_TYPES {
export enum EXTERNAL_SERVICE_TYPES {
REQUEST_ACCESS = "REQUEST_ACCESS",
SUBMIT_TRANSACTION = "SUBMIT_TRANSACTION",
SUBMIT_BLOB = "SUBMIT_BLOB",
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 @@ -5,5 +5,6 @@ describe("freighter API", () => {
expect(typeof FreighterAPI.isConnected).toBe("function");
expect(typeof FreighterAPI.getPublicKey).toBe("function");
expect(typeof FreighterAPI.signTransaction).toBe("function");
expect(typeof FreighterAPI.signBlob).toBe("function");
});
});
18 changes: 18 additions & 0 deletions @stellar/freighter-api/src/__tests__/signBlob.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as apiExternal from "@shared/api/external";
import { signBlob } from "../signTransaction";

describe("signBlob", () => {
it("returns a signed blob", async () => {
const TEST_BLOB = atob("AAA");
apiExternal.submitBlob = jest.fn().mockReturnValue(TEST_BLOB);
const blob = await signBlob();
expect(blob).toBe(TEST_BLOB);
});
it("throws a generic error", () => {
const TEST_ERROR = "Error!";
apiExternal.submitBlob = jest.fn().mockImplementation(() => {
throw TEST_ERROR;
});
expect(signBlob).toThrowError(TEST_ERROR);
});
});
3 changes: 3 additions & 0 deletions @stellar/freighter-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getPublicKey } from "./getPublicKey";
import { signTransaction } from "./signTransaction";
import { signBlob } from "./signBlob";
import { isConnected } from "./isConnected";
import { getNetwork } from "./getNetwork";
import { getNetworkDetails } from "./getNetworkDetails";
Expand All @@ -12,6 +13,7 @@ export const isBrowser = typeof window !== "undefined";
export {
getPublicKey,
signTransaction,
signBlob,
isConnected,
getNetwork,
getNetworkDetails,
Expand All @@ -22,6 +24,7 @@ export {
export default {
getPublicKey,
signTransaction,
signBlob,
isConnected,
getNetwork,
getNetworkDetails,
Expand Down
12 changes: 12 additions & 0 deletions @stellar/freighter-api/src/signBlob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { submitBlob } from "@shared/api/external";
import { isBrowser } from ".";

export const signBlob = (
blob: string,
opts: {
network: string;
networkPassphrase: string;
accountToSign: string;
}
): Promise<string> =>
isBrowser ? submitBlob(blob, opts) : Promise.resolve("");
11 changes: 11 additions & 0 deletions docs/docs/guide/usingFreighterNode.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
isConnected,
getPublicKey,
signTransaction,
signBlob,
} from "@stellar/freighter-api";
```

Expand Down Expand Up @@ -82,6 +83,7 @@ import {
isConnected,
getPublicKey,
signTransaction,
signBlob,
} from "@stellar/freighter-api";

if (await isConnected()) {
Expand Down Expand Up @@ -123,6 +125,7 @@ import {
setAllowed,
getUserInfo,
signTransaction,
signBlob,
} from "@stellar/freighter-api";

if (await isConnected()) {
Expand Down Expand Up @@ -175,6 +178,7 @@ import {
isConnected,
getNetwork,
signTransaction,
signBlob,
} from "@stellar/freighter-api";

if (await isConnected()) {
Expand Down Expand Up @@ -219,11 +223,18 @@ These 2 configurations are useful in the case that the user's Freighter is confi

You can also use this `opts` to specify which account's signature you’re requesting. If Freighter has the public key requested, it will switch to that account. If not, it will alert the user that they do not have the requested account.

### signBlob

#### `signBlob(xdr: string, opts: { network: string, networkPassphrase: string, accountToSign: string }) -> <Promise<string>>`

This is the same as `signTransaction` but accepts a base64 encoded blob.

```javascript
import {
isConnected,
getPublicKey,
signTransaction,
signBlob
} from "@stellar/freighter-api";

if (await isConnected()) {
Expand Down
216 changes: 130 additions & 86 deletions extension/src/background/messageListener/freighterApiMessageListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,95 +115,80 @@ export const freighterApiMessageListener = (
const isDomainListedAllowed = await isSenderAllowed({ sender });

// try to build a tx xdr, if you cannot then assume the user wants to sign an arbitrary blob
let encodedBlob = ""
try {
const transaction = SDK.TransactionBuilder.fromXDR(
transactionXdr,
networkPassphrase || SDK.Networks[network],
);

const directoryLookupJson = await cachedFetch(
STELLAR_EXPERT_BLOCKED_ACCOUNTS_URL,
CACHED_BLOCKED_ACCOUNTS_ID,
);
const accountData = directoryLookupJson?._embedded?.records || [];

const _operations =
transaction._operations || transaction._innerTransaction._operations;

const flaggedKeys: FlaggedKeys = {};

const isValidatingMemo = (await getIsMemoValidationEnabled()) && isMainnet;
const isValidatingSafety =
(await getIsSafetyValidationEnabled()) && isMainnet;

if (isValidatingMemo || isValidatingSafety) {
_operations.forEach((operation: { destination: string }) => {
accountData.forEach(
({ address, tags }: { address: string; tags: Array<string> }) => {
if (address === operation.destination) {
let collectedTags = [...tags];

/* if the user has opted out of validation, remove applicable tags */
if (!isValidatingMemo) {
collectedTags.filter(
(tag) => tag !== TRANSACTION_WARNING.memoRequired,
);
}
if (!isValidatingSafety) {
collectedTags = collectedTags.filter(
(tag) => tag !== TRANSACTION_WARNING.unsafe,
);
collectedTags = collectedTags.filter(
(tag) => tag !== TRANSACTION_WARNING.malicious,
);
}
flaggedKeys[operation.destination] = {
...flaggedKeys[operation.destination],
tags: collectedTags,
};
const transaction = SDK.TransactionBuilder.fromXDR(
transactionXdr,
networkPassphrase || SDK.Networks[network],
);

const directoryLookupJson = await cachedFetch(
STELLAR_EXPERT_BLOCKED_ACCOUNTS_URL,
CACHED_BLOCKED_ACCOUNTS_ID,
);
const accountData = directoryLookupJson?._embedded?.records || [];

const _operations =
transaction._operations || transaction._innerTransaction._operations;

const flaggedKeys: FlaggedKeys = {};

const isValidatingMemo = (await getIsMemoValidationEnabled()) && isMainnet;
const isValidatingSafety =
(await getIsSafetyValidationEnabled()) && isMainnet;

if (isValidatingMemo || isValidatingSafety) {
_operations.forEach((operation: { destination: string }) => {
accountData.forEach(
({ address, tags }: { address: string; tags: Array<string> }) => {
if (address === operation.destination) {
let collectedTags = [...tags];

/* if the user has opted out of validation, remove applicable tags */
if (!isValidatingMemo) {
collectedTags.filter(
(tag) => tag !== TRANSACTION_WARNING.memoRequired,
);
}
},
);
});
}

const server = stellarSdkServer(networkUrl);

try {
await server.checkMemoRequired(transaction);
} catch (e) {
flaggedKeys[e.accountId] = {
...flaggedKeys[e.accountId],
tags: [TRANSACTION_WARNING.memoRequired],
};
}

const transactionInfo = {
transaction,
transactionXdr,
tab,
isDomainListedAllowed,
url: tabUrl,
flaggedKeys,
accountToSign,
} as TransactionInfo;

transactionQueue.push(transaction);
encodedBlob = encodeObject(transactionInfo);
} catch (error) {
const blob = {
isDomainListedAllowed,
domain,
tab,
blob: transactionXdr,
url: tabUrl,
accountToSign
}
if (!isValidatingSafety) {
collectedTags = collectedTags.filter(
(tag) => tag !== TRANSACTION_WARNING.unsafe,
);
collectedTags = collectedTags.filter(
(tag) => tag !== TRANSACTION_WARNING.malicious,
);
}
flaggedKeys[operation.destination] = {
...flaggedKeys[operation.destination],
tags: collectedTags,
};
}
},
);
});
}

const server = stellarSdkServer(networkUrl);

blobQueue.push(blob);
encodedBlob = encodeObject(blob)
try {
await server.checkMemoRequired(transaction);
} catch (e) {
flaggedKeys[e.accountId] = {
...flaggedKeys[e.accountId],
tags: [TRANSACTION_WARNING.memoRequired],
};
}

const transactionInfo = {
transaction,
transactionXdr,
tab,
isDomainListedAllowed,
url: tabUrl,
flaggedKeys,
accountToSign,
} as TransactionInfo;

transactionQueue.push(transaction);
const encodedBlob = encodeObject(transactionInfo);

const popup = browser.windows.create({
url: chrome.runtime.getURL(
Expand Down Expand Up @@ -238,6 +223,64 @@ export const freighterApiMessageListener = (
});
};

const submitBlob = async () => {
const {
transactionXdr,
accountToSign,
} = request;

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 blob = {
isDomainListedAllowed,
domain,
tab,
blob: transactionXdr,
url: tabUrl,
accountToSign
}

blobQueue.push(blob);
const encodedBlob = encodeObject(blob)
const popup = browser.windows.create({
url: chrome.runtime.getURL(
`/index.html#/sign-transaction?${encodedBlob}`,
),
...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 = (signedBlob: string) => {
if (signedBlob) {
if (!isDomainListedAllowed) {
allowList.push(punycodedDomain);
localStore.setItem(ALLOWLIST_ID, allowList.join());
}
resolve({ signedBlob });
}

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

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

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

Expand Down Expand Up @@ -329,6 +372,7 @@ export const freighterApiMessageListener = (
const messageResponder: MessageResponder = {
[EXTERNAL_SERVICE_TYPES.REQUEST_ACCESS]: requestAccess,
[EXTERNAL_SERVICE_TYPES.SUBMIT_TRANSACTION]: submitTransaction,
[EXTERNAL_SERVICE_TYPES.SUBMIT_BLOB]: submitBlob,
[EXTERNAL_SERVICE_TYPES.REQUEST_NETWORK]: requestNetwork,
[EXTERNAL_SERVICE_TYPES.REQUEST_NETWORK_DETAILS]: requestNetworkDetails,
[EXTERNAL_SERVICE_TYPES.REQUEST_CONNECTION_STATUS]: requestConnectionStatus,
Expand Down
Loading

0 comments on commit f3865d3

Please sign in to comment.