diff --git a/@shared/api/external.ts b/@shared/api/external.ts index 52ebedd38d..1ca5774e3c 100644 --- a/@shared/api/external.ts +++ b/@shared/api/external.ts @@ -60,6 +60,7 @@ export const submitTransaction = async ( }); } catch (e) { console.error(e); + throw e; } const { signedTransaction, error } = response; @@ -86,6 +87,7 @@ export const submitBlob = async ( }); } catch (e) { console.error(e); + throw e; } const { signedBlob, error } = response; @@ -95,6 +97,32 @@ export const submitBlob = async ( return signedBlob; }; +export const submitAuthEntry = async ( + entryXdr: string, + opts?: { + accountToSign?: string; + }, +): Promise => { + 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); + } + const { signedAuthEntry, error } = response; + + if (error) { + throw error; + } + return signedAuthEntry; +}; + export const requestNetwork = async (): Promise => { let response = { network: "", error: "" }; try { diff --git a/@shared/api/helpers/soroban.ts b/@shared/api/helpers/soroban.ts index 1227d49de7..da428db8a7 100644 --- a/@shared/api/helpers/soroban.ts +++ b/@shared/api/helpers/soroban.ts @@ -1,7 +1,5 @@ import { xdr, Address, ScInt, scValToBigInt } from "soroban-client"; -/* eslint-disable */ - export const accountIdentifier = (account: string) => new Address(account).toScVal(); @@ -27,5 +25,3 @@ export const decodeU32 = (b64: string) => export const numberToI128 = (value: number): xdr.ScVal => new ScInt(value).toI128(); - -/* eslint-enable */ diff --git a/@shared/api/internal.ts b/@shared/api/internal.ts index 44f0368855..582b1ca1e2 100644 --- a/@shared/api/internal.ts +++ b/@shared/api/internal.ts @@ -1,6 +1,12 @@ import StellarSdk from "stellar-sdk"; import * as SorobanClient from "soroban-client"; import { DataProvider } from "@stellar/wallet-sdk"; +import { + getBalance, + getDecimals, + getName, + getSymbol, +} from "@shared/helpers/soroban/token"; import { Account, AccountBalancesInterface, @@ -8,12 +14,12 @@ import { Balances, HorizonOperation, Settings, - SorobanTxStatus, } from "./types"; import { MAINNET_NETWORK_DETAILS, DEFAULT_NETWORKS, NetworkDetails, + NETWORKS, SOROBAN_RPC_URLS, } from "../constants/stellar"; import { SERVICE_TYPES } from "../constants/services"; @@ -24,10 +30,25 @@ import { getIconUrlFromIssuer } from "./helpers/getIconUrlFromIssuer"; import { getDomainFromIssuer } from "./helpers/getDomainFromIssuer"; import { stellarSdkServer } from "./helpers/stellarSdkServer"; -import { decodei128, decodeU32, decodeStr } from "./helpers/soroban"; - const TRANSACTIONS_LIMIT = 100; +export const SendTxStatus: { + [index: string]: SorobanClient.SorobanRpc.SendTransactionStatus; +} = { + Pending: "PENDING", + Duplicate: "DUPLICATE", + Retry: "TRY_AGAIN_LATER", + Error: "ERROR", +}; + +export const GetTxStatus: { + [index: string]: SorobanClient.SorobanRpc.GetTransactionStatus; +} = { + Success: SorobanClient.SorobanRpc.GetTransactionStatus.SUCCESS, + NotFound: SorobanClient.SorobanRpc.GetTransactionStatus.NOT_FOUND, + Failed: SorobanClient.SorobanRpc.GetTransactionStatus.FAILED, +}; + export const createAccount = async ( password: string, ): Promise<{ publicKey: string; allAccounts: Array }> => { @@ -515,6 +536,16 @@ export const signBlob = async (): Promise => { } }; +export const signAuthEntry = async (): Promise => { + try { + await sendMessageToBackground({ + type: SERVICE_TYPES.SIGN_AUTH_ENTRY, + }); + } catch (e) { + console.error(e); + } +}; + export const signFreighterTransaction = async ({ transactionXDR, network, @@ -606,28 +637,49 @@ export const submitFreighterSorobanTransaction = async ({ console.error(e); } - const server = new SorobanClient.Server(SOROBAN_RPC_URLS.FUTURENET, { - allowHttp: true, + if ( + !networkDetails.sorobanRpcUrl && + networkDetails.network !== NETWORKS.FUTURENET + ) { + throw new Error("soroban rpc not supported"); + } + + // TODO: after enough time has passed to assume most clients have ran + // the migrateSorobanRpcUrlNetworkDetails migration, remove and use networkDetails.sorobanRpcUrl + const serverUrl = !networkDetails.sorobanRpcUrl + ? SOROBAN_RPC_URLS[NETWORKS.FUTURENET]! + : networkDetails.sorobanRpcUrl; + + const server = new SorobanClient.Server(serverUrl, { + allowHttp: !serverUrl.startsWith("https"), }); - // TODO: fixed in Sorobanclient, not yet released - let response = (await server.sendTransaction(tx)) as any; + let response = await server.sendTransaction(tx); - try { - // Poll this until the status is not "pending" - while (response.status === SorobanTxStatus.PENDING) { + if (response.errorResultXdr) { + throw new Error(response.errorResultXdr); + } + + if (response.status === SendTxStatus.Pending) { + let txResponse = await server.getTransaction(response.hash); + + // Poll this until the status is not "NOT_FOUND" + while (txResponse.status === GetTxStatus.NotFound) { // See if the transaction is complete // eslint-disable-next-line no-await-in-loop - response = await server.getTransaction(response.id); + txResponse = await server.getTransaction(response.hash); // Wait a second // eslint-disable-next-line no-await-in-loop await new Promise((resolve) => setTimeout(resolve, 1000)); } - } catch (e) { - throw new Error(e); - } - return response; + return response; + // eslint-disable-next-line no-else-return + } else { + throw new Error( + `Unabled to submit transaction, status: ${response.status}`, + ); + } }; export const addRecentAddress = async ({ @@ -859,98 +911,42 @@ export const getBlockedAccounts = async () => { return resp; }; -type TxToOp = { - [index: string]: { - tx: SorobanClient.Transaction< - SorobanClient.Memo, - SorobanClient.Operation[] - >; - decoder: (xdr: string) => string | number; - }; -}; - -interface SorobanTokenRecord { - [key: string]: unknown; - balance: number; - name: string; - symbol: string; - decimals: string; -} - -export const getSorobanTokenBalance = ( +export const getSorobanTokenBalance = async ( server: SorobanClient.Server, contractId: string, txBuilders: { - // need a builder per operation until multi-op transactions are released + // need a builder per operation, Soroban currently has single op transactions balance: SorobanClient.TransactionBuilder; name: SorobanClient.TransactionBuilder; decimals: SorobanClient.TransactionBuilder; symbol: SorobanClient.TransactionBuilder; }, - params: SorobanClient.xdr.ScVal[], + balanceParams: SorobanClient.xdr.ScVal[], ) => { - const contract = new SorobanClient.Contract(contractId); - // Right now we can only have 1 operation per TX in Soroban - // There is ongoing work to lift this restriction - // but for now we need to do 4 txs to show 1 user balance. :( - const balanceTx = txBuilders.balance - .addOperation(contract.call("balance", ...params)) - .setTimeout(SorobanClient.TimeoutInfinite) - .build(); - - const nameTx = txBuilders.name - .addOperation(contract.call("name")) - .setTimeout(SorobanClient.TimeoutInfinite) - .build(); - - const symbolTx = txBuilders.symbol - .addOperation(contract.call("symbol")) - .setTimeout(SorobanClient.TimeoutInfinite) - .build(); - - const decimalsTx = txBuilders.decimals - .addOperation(contract.call("decimals")) - .setTimeout(SorobanClient.TimeoutInfinite) - .build(); - - const txs: TxToOp = { - balance: { - tx: balanceTx, - decoder: decodei128, - }, - name: { - tx: nameTx, - decoder: decodeStr, - }, - symbol: { - tx: symbolTx, - decoder: decodeStr, - }, - decimals: { - tx: decimalsTx, - decoder: decodeU32, - }, - }; - - const tokenBalanceInfo = Object.keys(txs).reduce(async (prev, curr) => { - const _prev = await prev; - const { tx, decoder } = txs[curr]; - const { results } = await server.simulateTransaction(tx); - if (!results || results.length !== 1) { - throw new Error("Invalid response from simulateTransaction"); - } - const result = results[0]; - _prev[curr] = decoder(result.xdr); - - return _prev; - }, Promise.resolve({} as SorobanTokenRecord)); + // for now we need to do 4 tx simulations to show 1 user balance. :( + // TODO: figure out how to fetch ledger keys to do this more efficiently + const decimals = await getDecimals(contractId, server, txBuilders.decimals); + const name = await getName(contractId, server, txBuilders.name); + const symbol = await getSymbol(contractId, server, txBuilders.symbol); + const balance = await getBalance( + contractId, + balanceParams, + server, + txBuilders.balance, + ); - return tokenBalanceInfo; + return { + balance, + decimals, + name, + symbol, + }; }; export const addTokenId = async ( tokenId: string, + network: SorobanClient.Networks, ): Promise<{ tokenIdList: string[]; }> => { @@ -960,6 +956,7 @@ export const addTokenId = async ( try { ({ tokenIdList, error } = await sendMessageToBackground({ tokenId, + network, type: SERVICE_TYPES.ADD_TOKEN_ID, })); } catch (e) { @@ -973,9 +970,12 @@ export const addTokenId = async ( return { tokenIdList }; }; -export const getTokenIds = async (): Promise => { +export const getTokenIds = async ( + network: SorobanClient.Networks, +): Promise => { const resp = await sendMessageToBackground({ type: SERVICE_TYPES.GET_TOKEN_IDS, + network, }); return resp.tokenIdList; }; diff --git a/@shared/api/package.json b/@shared/api/package.json index fac3765ebc..f6030e898f 100644 --- a/@shared/api/package.json +++ b/@shared/api/package.json @@ -7,7 +7,7 @@ "@stellar/wallet-sdk": "^0.8.0", "bignumber.js": "^9.1.1", "prettier": "^2.0.5", - "soroban-client": "^0.9.1", + "soroban-client": "^1.0.0-beta.2", "stellar-sdk": "^10.4.1", "typescript": "~3.7.2", "webextension-polyfill": "^0.10.0" diff --git a/@shared/api/types.ts b/@shared/api/types.ts index b5d25d3d91..611b52a610 100644 --- a/@shared/api/types.ts +++ b/@shared/api/types.ts @@ -14,11 +14,6 @@ export enum ActionStatus { ERROR = "ERROR", } -export enum SorobanTxStatus { - PENDING = "pending", - SUCCESS = "success", -} - export interface UserInfo { publicKey: string; } @@ -42,6 +37,7 @@ export interface Response { transactionXDR: string; signedTransaction: string; signedBlob: string; + signedAuthEntry: string; source: string; type: SERVICE_TYPES; url: string; @@ -52,6 +48,7 @@ export interface Response { isValidatingSafeAssetsEnabled: boolean; isExperimentalModeEnabled: boolean; networkDetails: NetworkDetails; + sorobanRpcUrl: string; networksList: NetworkDetails[]; allAccounts: Array; accountName: string; @@ -101,7 +98,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; @@ -148,7 +152,7 @@ export interface SorobanBalance { total: BigNumber; name: string; symbol: string; - decimals: string; + decimals: number; } export type AssetType = diff --git a/@shared/constants/services.ts b/@shared/constants/services.ts index 8649889785..21a310d8ff 100644 --- a/@shared/constants/services.ts +++ b/@shared/constants/services.ts @@ -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", @@ -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", diff --git a/@shared/constants/soroban/token.ts b/@shared/constants/soroban/token.ts new file mode 100644 index 0000000000..958cdfafcf --- /dev/null +++ b/@shared/constants/soroban/token.ts @@ -0,0 +1,17 @@ +// https://github.com/stellar/soroban-examples/blob/main/token/src/contract.rs +export enum SorobanTokenInterface { + transfer = "transfer", + mint = "mint", +} + +// TODO: can we generate this at build time using the cli TS generator? Also should we? +export interface SorobanToken { + // only currently holds fields we care about + transfer: (from: string, to: string, amount: number) => void; + mint: (to: string, amount: number) => void; + // values below are in storage + name: string; + balance: number; + symbol: string; + decimals: number; +} diff --git a/@shared/constants/stellar.ts b/@shared/constants/stellar.ts index ba8dff6c24..51796b332a 100644 --- a/@shared/constants/stellar.ts +++ b/@shared/constants/stellar.ts @@ -23,8 +23,8 @@ export enum FRIENDBOT_URLS { FUTURENET = "https://friendbot-futurenet.stellar.org", } -export const SOROBAN_RPC_URLS = { - FUTURENET: "https://rpc-futurenet.stellar.org/", +export const SOROBAN_RPC_URLS: { [key in NETWORKS]?: string } = { + [NETWORKS.FUTURENET]: "https://rpc-futurenet.stellar.org/", }; export interface NetworkDetails { @@ -33,6 +33,7 @@ export interface NetworkDetails { networkUrl: string; networkPassphrase: string; friendbotUrl?: string; + sorobanRpcUrl?: string; } export const MAINNET_NETWORK_DETAILS: NetworkDetails = { diff --git a/@shared/helpers/package.json b/@shared/helpers/package.json index c08fba0b9a..ac7ba0ede4 100644 --- a/@shared/helpers/package.json +++ b/@shared/helpers/package.json @@ -4,6 +4,7 @@ "version": "1.0.0", "dependencies": { "typescript": "~3.7.2", + "soroban-client": "^1.0.0-beta.2", "stellar-sdk": "^10.4.1" }, "devDependencies": { diff --git a/@shared/helpers/soroban/__tests__/server.test.ts b/@shared/helpers/soroban/__tests__/server.test.ts new file mode 100644 index 0000000000..b65a4d4de2 --- /dev/null +++ b/@shared/helpers/soroban/__tests__/server.test.ts @@ -0,0 +1,44 @@ +import { + Memo, + MemoType, + Operation, + Server, + nativeToScVal, + Transaction, + TransactionBuilder, +} from "soroban-client"; +import { simulateTx } from "../server"; + +export const FUTURENET_DETAILS = { + network: "FUTURENET", + networkUrl: "https://horizon-futurenet.stellar.org", + networkPassphrase: "Test SDF Future Network ; October 2022", +}; + +describe("Soroban Helpers - ", () => { + describe("simulateTx", () => { + const TEST_XDR = + "AAAAAgAAAABngBTmbmUycqG2cAMHcomSR80dRzGtKzxM6gb3yySD5AAAAGQAAAUcAAAABgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAAByJKWoF9C6Tt+//t+9ocHp4kExcoTwAseKdAcKEUBTXAAAAAHYmFsYW5jZQAAAAABAAAAEgAAAAAAAAAAZ4AU5m5lMnKhtnADB3KJkkfNHUcxrSs8TOoG98skg+QAAAAAAAAAAAAAAAA="; + const testTx = TransactionBuilder.fromXDR( + TEST_XDR, + FUTURENET_DETAILS.networkPassphrase, + ) as Transaction, Operation[]>; + const mockSimResult = { + auth: [], + retval: nativeToScVal(100), + }; + const mockSim = jest.fn( + (_tx: Transaction, Operation[]>) => ({ + result: mockSimResult, + }), + ); + const mockServer = ({ + simulateTransaction: mockSim, + } as any) as Server; + + test("should take tx/server and return a native type according to the generic type argument", async () => { + const result = await simulateTx(testTx, mockServer); + expect(typeof result).toEqual("bigint"); + }); + }); +}); diff --git a/@shared/helpers/soroban/server.ts b/@shared/helpers/soroban/server.ts new file mode 100644 index 0000000000..d29c7aa1a3 --- /dev/null +++ b/@shared/helpers/soroban/server.ts @@ -0,0 +1,21 @@ +import { + Transaction, + Memo, + MemoType, + Operation, + Server, + scValToNative, +} from "soroban-client"; + +export const simulateTx = async ( + tx: Transaction, Operation[]>, + server: Server, +): Promise => { + const simulatedTX = await server.simulateTransaction(tx); + + if ("result" in simulatedTX && simulatedTX.result !== undefined) { + return scValToNative(simulatedTX.result.retval); + } + + throw new Error("Invalid response from simulateTransaction"); +}; diff --git a/@shared/helpers/soroban/token.ts b/@shared/helpers/soroban/token.ts new file mode 100644 index 0000000000..26970917ad --- /dev/null +++ b/@shared/helpers/soroban/token.ts @@ -0,0 +1,93 @@ +import { + Contract, + TransactionBuilder, + Memo, + Server, + TimeoutInfinite, + xdr, +} from "soroban-client"; +import { simulateTx } from "./server"; + +export const transfer = ( + contractId: string, + params: xdr.ScVal[], + memo: string | undefined, + builder: TransactionBuilder, +) => { + const contract = new Contract(contractId); + + const tx = builder + .addOperation(contract.call("transfer", ...params)) + .setTimeout(TimeoutInfinite); + + if (memo) { + tx.addMemo(Memo.text(memo)); + } + + return tx.build(); +}; + +export const getBalance = async ( + contractId: string, + params: xdr.ScVal[], + server: Server, + builder: TransactionBuilder, +) => { + const contract = new Contract(contractId); + + const tx = builder + .addOperation(contract.call("balance", ...params)) + .setTimeout(TimeoutInfinite) + .build(); + + const result = await simulateTx(tx, server); + return result; +}; + +export const getDecimals = async ( + contractId: string, + server: Server, + builder: TransactionBuilder, +) => { + const contract = new Contract(contractId); + + const tx = builder + .addOperation(contract.call("decimals")) + .setTimeout(TimeoutInfinite) + .build(); + + const result = await simulateTx(tx, server); + return result; +}; + +export const getName = async ( + contractId: string, + server: Server, + builder: TransactionBuilder, +) => { + const contract = new Contract(contractId); + + const tx = builder + .addOperation(contract.call("name")) + .setTimeout(TimeoutInfinite) + .build(); + + const result = await simulateTx(tx, server); + return result; +}; + +export const getSymbol = async ( + contractId: string, + server: Server, + builder: TransactionBuilder, +) => { + const contract = new Contract(contractId); + + const tx = builder + .addOperation(contract.call("symbol")) + .setTimeout(TimeoutInfinite) + .build(); + + const result = await simulateTx(tx, server); + return result; +}; diff --git a/@shared/helpers/types.ts b/@shared/helpers/types.ts new file mode 100644 index 0000000000..d1790a5a18 --- /dev/null +++ b/@shared/helpers/types.ts @@ -0,0 +1 @@ +export type WithRequired = T & { [P in K]-?: T[P] }; diff --git a/@stellar/freighter-api/src/__tests__/index.test.js b/@stellar/freighter-api/src/__tests__/index.test.js index 5de51117e9..9986bc6813 100644 --- a/@stellar/freighter-api/src/__tests__/index.test.js +++ b/@stellar/freighter-api/src/__tests__/index.test.js @@ -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"); }); }); diff --git a/@stellar/freighter-api/src/__tests__/signBlob.test.js b/@stellar/freighter-api/src/__tests__/signBlob.test.js index 55dc0512c6..272b08c105 100644 --- a/@stellar/freighter-api/src/__tests__/signBlob.test.js +++ b/@stellar/freighter-api/src/__tests__/signBlob.test.js @@ -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(() => { diff --git a/@stellar/freighter-api/src/index.ts b/@stellar/freighter-api/src/index.ts index 66909f9258..31162a5c33 100644 --- a/@stellar/freighter-api/src/index.ts +++ b/@stellar/freighter-api/src/index.ts @@ -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"; @@ -14,6 +15,7 @@ export { getPublicKey, signTransaction, signBlob, + signAuthEntry, isConnected, getNetwork, getNetworkDetails, @@ -25,6 +27,7 @@ export default { getPublicKey, signTransaction, signBlob, + signAuthEntry, isConnected, getNetwork, getNetworkDetails, diff --git a/@stellar/freighter-api/src/signAuthEntry.ts b/@stellar/freighter-api/src/signAuthEntry.ts new file mode 100644 index 0000000000..dd14cdf8f0 --- /dev/null +++ b/@stellar/freighter-api/src/signAuthEntry.ts @@ -0,0 +1,10 @@ +import { submitAuthEntry } from "@shared/api/external"; +import { isBrowser } from "."; + +export const signAuthEntry = ( + entryXdr: string, + opts?: { + accountToSign?: string; + } +): Promise => + isBrowser ? submitAuthEntry(entryXdr, opts) : Promise.resolve(""); diff --git a/docs/docs/guide/usingFreighterNode.md b/docs/docs/guide/usingFreighterNode.md index 77292bfee6..e6ed686ad0 100644 --- a/docs/docs/guide/usingFreighterNode.md +++ b/docs/docs/guide/usingFreighterNode.md @@ -19,6 +19,7 @@ or import just the modules you require: import { isConnected, getPublicKey, + signAuthEntry, signTransaction, signBlob, } from "@stellar/freighter-api"; @@ -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"; @@ -124,6 +126,7 @@ import { isAllowed, setAllowed, getUserInfo, + signAuthEntry, signTransaction, signBlob, } from "@stellar/freighter-api"; @@ -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"; @@ -234,7 +238,7 @@ import { isConnected, getPublicKey, signTransaction, - signBlob + signBlob, } from "@stellar/freighter-api"; if (await isConnected()) { diff --git a/extension/package.json b/extension/package.json index be939f19e2..03241f29cb 100755 --- a/extension/package.json +++ b/extension/package.json @@ -71,9 +71,10 @@ "redux": "^4.0.5", "sass": "^1.22.10", "sass-loader": "8.0.0", + "semver": "^7.5.4", "ses": "^0.18.5", "simplebar-react": "^2.3.6", - "soroban-client": "^0.9.1", + "soroban-client": "^1.0.0-beta.2", "stellar-hd-wallet": "^0.0.10", "stellar-identicon-js": "^1.0.0", "stellar-sdk": "^10.4.1", @@ -85,6 +86,7 @@ }, "devDependencies": { "@lavamoat/allow-scripts": "^2.3.1", + "@types/semver": "^7.5.2", "https-browserify": "^1.0.0", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", diff --git a/extension/src/background/helpers/dataStorage.ts b/extension/src/background/helpers/dataStorage.ts index 99a5f3a11b..5af5be3bc6 100644 --- a/extension/src/background/helpers/dataStorage.ts +++ b/extension/src/background/helpers/dataStorage.ts @@ -1,12 +1,18 @@ import browser from "webextension-polyfill"; +import semver from "semver"; -import { NETWORKS_LIST_ID } from "constants/localStorageTypes"; +import { + NETWORKS_LIST_ID, + STORAGE_VERSION, + TOKEN_ID_LIST, +} from "constants/localStorageTypes"; import { DEFAULT_NETWORKS, NetworkDetails, NETWORKS, TESTNET_NETWORK_DETAILS, FUTURENET_NETWORK_DETAILS, + SOROBAN_RPC_URLS, } from "@shared/constants/stellar"; interface SetItemParams { @@ -105,3 +111,50 @@ export const migrateFriendBotUrlNetworkDetails = async () => { await localStore.setItem(NETWORKS_LIST_ID, migratedNetworkList); }; + +export const migrateSorobanRpcUrlNetworkDetails = async () => { + const localStore = dataStorageAccess(browserLocalStorage); + + const networksList: NetworkDetails[] = + (await localStore.getItem(NETWORKS_LIST_ID)) || DEFAULT_NETWORKS; + + const migratedNetworkList = networksList.map((network) => { + if (network.network === NETWORKS.FUTURENET) { + return { + ...FUTURENET_NETWORK_DETAILS, + sorobanRpcUrl: SOROBAN_RPC_URLS[NETWORKS.FUTURENET], + }; + } + + return network; + }); + + await localStore.setItem(NETWORKS_LIST_ID, migratedNetworkList); +}; + +// This migration migrates the storage for custom tokens IDs to be keyed by network +export const migrateTokenIdList = async () => { + const localStore = dataStorageAccess(browserLocalStorage); + const tokenIdsByKey = (await localStore.getItem(TOKEN_ID_LIST)) as Record< + string, + object + >; + const storageVersion = (await localStore.getItem(STORAGE_VERSION)) as string; + + if (!storageVersion || semver.lt(storageVersion, "1.0.0")) { + const newTokenList = { + [NETWORKS.FUTURENET]: tokenIdsByKey, + }; + await localStore.setItem(TOKEN_ID_LIST, newTokenList); + } + await migrateDataStorageVersion(); +}; + +// Updates storage version +export const migrateDataStorageVersion = async () => { + const localStore = dataStorageAccess(browserLocalStorage); + + // This value should be manually updated when a new schema change is made + const STORAGE_SCHEMA_VERSION = "1.0.0"; + await localStore.setItem(STORAGE_VERSION, STORAGE_SCHEMA_VERSION); +}; diff --git a/extension/src/background/index.ts b/extension/src/background/index.ts index a82d40b815..502dc4ad42 100644 --- a/extension/src/background/index.ts +++ b/extension/src/background/index.ts @@ -13,6 +13,8 @@ import { timeoutAccountAccess } from "./ducks/session"; import { migrateFriendBotUrlNetworkDetails, normalizeMigratedData, + migrateSorobanRpcUrlNetworkDetails, + migrateTokenIdList, } from "./helpers/dataStorage"; export const initContentScriptMessageListener = () => { @@ -56,6 +58,8 @@ export const initInstalledListener = () => { }); browser?.runtime?.onInstalled.addListener(normalizeMigratedData); browser?.runtime?.onInstalled.addListener(migrateFriendBotUrlNetworkDetails); + browser?.runtime?.onInstalled.addListener(migrateSorobanRpcUrlNetworkDetails); + browser?.runtime?.onInstalled.addListener(migrateTokenIdList); }; export const initAlarmListener = (sessionStore: Store) => { diff --git a/extension/src/background/messageListener/freighterApiMessageListener.ts b/extension/src/background/messageListener/freighterApiMessageListener.ts index 9357cae3de..a554190ca4 100644 --- a/extension/src/background/messageListener/freighterApiMessageListener.ts +++ b/extension/src/background/messageListener/freighterApiMessageListener.ts @@ -4,6 +4,7 @@ import browser from "webextension-polyfill"; import { Store } from "redux"; import { + ExternalRequestAuthEntry, ExternalRequestBlob, ExternalRequestTx, ExternalRequest as Request, @@ -39,6 +40,7 @@ import { import { publicKeySelector } from "background/ducks/session"; import { + authEntryQueue, blobQueue, responseQueue, transactionQueue, @@ -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, }); @@ -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 = ""; @@ -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, diff --git a/extension/src/background/messageListener/popupMessageListener.ts b/extension/src/background/messageListener/popupMessageListener.ts index 905fa4f9c6..896f399e4c 100644 --- a/extension/src/background/messageListener/popupMessageListener.ts +++ b/extension/src/background/messageListener/popupMessageListener.ts @@ -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; @@ -949,6 +956,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 {}; + } + } + + return { error: "Session timed out" }; + }; + const rejectTransaction = () => { transactionQueue.pop(); const response = responseQueue.pop(); @@ -1194,8 +1224,9 @@ export const popupMessageListener = (request: Request, sessionStore: Store) => { }; const addTokenId = async () => { - const { tokenId } = request; - const tokenIdList = (await localStore.getItem(TOKEN_ID_LIST)) || {}; + const { tokenId, network } = request; + const tokenIdsByNetwork = (await localStore.getItem(TOKEN_ID_LIST)) || {}; + const tokenIdList = tokenIdsByNetwork[network] || {}; const keyId = (await localStore.getItem(KEY_ID)) || ""; const accountTokenIdList = tokenIdList[keyId] || []; @@ -1206,18 +1237,23 @@ export const popupMessageListener = (request: Request, sessionStore: Store) => { accountTokenIdList.push(tokenId); await localStore.setItem(TOKEN_ID_LIST, { - ...tokenIdList, - [keyId]: accountTokenIdList, + ...tokenIdsByNetwork, + [network]: { + ...tokenIdList, + [keyId]: accountTokenIdList, + }, }); return { accountTokenIdList }; }; const getTokenIds = async () => { - const tokenIdList = (await localStore.getItem(TOKEN_ID_LIST)) || {}; + const { network } = request; + const tokenIdsByNetwork = (await localStore.getItem(TOKEN_ID_LIST)) || {}; + const tokenIdsByKey = tokenIdsByNetwork[network] || {}; const keyId = (await localStore.getItem(KEY_ID)) || ""; - return { tokenIdList: tokenIdList[keyId] || [] }; + return { tokenIdList: tokenIdsByKey[keyId] || [] }; }; const messageResponder: MessageResponder = { @@ -1241,6 +1277,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, diff --git a/extension/src/constants/localStorageTypes.ts b/extension/src/constants/localStorageTypes.ts index e54bf82dbb..1ee369e7dd 100644 --- a/extension/src/constants/localStorageTypes.ts +++ b/extension/src/constants/localStorageTypes.ts @@ -18,3 +18,4 @@ export const NETWORK_ID = "network"; export const NETWORKS_LIST_ID = "networksList"; export const METRICS_DATA = "metricsData"; export const TOKEN_ID_LIST = "tokenIdList"; +export const STORAGE_VERSION = "storageVersion"; diff --git a/extension/src/constants/transaction.ts b/extension/src/constants/transaction.ts index 9c114b5251..05a0929995 100644 --- a/extension/src/constants/transaction.ts +++ b/extension/src/constants/transaction.ts @@ -28,6 +28,8 @@ export enum OPERATION_TYPES { revokeTrustlineSponsorship = "Revoke Trustline Sponsorship", setOptions = "Set Options", setTrustLineFlags = "Set Trustline Flags", + bumpFootprintExpiration = "Bump Footprint Expiration", + restoreFootprint = "Restore Footprint", } export enum TRANSACTION_WARNING { diff --git a/extension/src/helpers/__tests__/stellar.test.ts b/extension/src/helpers/__tests__/stellar.test.ts index 24afd294e4..eec08bde02 100644 --- a/extension/src/helpers/__tests__/stellar.test.ts +++ b/extension/src/helpers/__tests__/stellar.test.ts @@ -1,4 +1,10 @@ -import { getTransactionInfo, truncatedPublicKey } from "../stellar"; +import BigNumber from "bignumber.js"; +import { + getTransactionInfo, + truncatedPublicKey, + stroopToXlm, + xlmToStroop, +} from "../stellar"; import * as urls from "../urls"; describe("truncatedPublicKey", () => { @@ -26,7 +32,7 @@ describe("getTransactionInfo", () => { tab: {}, }); const info = getTransactionInfo("foo"); - if (!("blob" in info)) { + if (!("blob" in info) && !("entry" in info)) { expect(info.isHttpsDomain).toBe(true); } }); @@ -41,8 +47,47 @@ describe("getTransactionInfo", () => { tab: {}, }); const info = getTransactionInfo("foo"); - if (!("blob" in info)) { + if (!("blob" in info) && !("entry" in info)) { expect(info.isHttpsDomain).toBe(false); } }); }); + +describe("stroopToXlm", () => { + test("should convert a raw string representing stroops into the equivalent value in lumens", () => { + const stroops = "10000001"; + const lumens = stroopToXlm(stroops); + + expect(lumens).toEqual(new BigNumber(Number(stroops) / 1e7)); + }); + + test("should convert a raw number representing stroops into the equivalent value in lumens", () => { + const stroops = 10000001; + const lumens = stroopToXlm(stroops); + + expect(lumens).toEqual(new BigNumber(Number(stroops) / 1e7)); + }); + + test("should convert a BigNumber representing stroops into the equivalent value in lumens", () => { + const stroops = new BigNumber("10000001"); + const lumens = stroopToXlm(stroops); + + expect(lumens).toEqual(stroops.dividedBy(1e7)); + }); +}); + +describe("xlmToStroop", () => { + test("should convert a raw string representing a value in lumens to its equivalent value in stroops", () => { + const lumens = "11"; + const stroops = xlmToStroop(lumens); + + expect(stroops).toEqual(new BigNumber(Math.round(Number(lumens) * 1e7))); + }); + + test("should convert a BigNumber representing a value in lumens to its equivalent value in stroops", () => { + const lumens = new BigNumber("11"); + const stroops = xlmToStroop(lumens); + + expect(stroops).toEqual(lumens.times(1e7)); + }); +}); diff --git a/extension/src/helpers/stellar.ts b/extension/src/helpers/stellar.ts index c714daab6c..ca18c5977d 100644 --- a/extension/src/helpers/stellar.ts +++ b/extension/src/helpers/stellar.ts @@ -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 @@ -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, diff --git a/extension/src/helpers/urls.ts b/extension/src/helpers/urls.ts index d3296dd3ea..9a1107af98 100644 --- a/extension/src/helpers/urls.ts +++ b/extension/src/helpers/urls.ts @@ -5,12 +5,21 @@ import { TransactionInfo } from "../types/transactions"; export interface BlobToSign { isDomainListedAllowed: boolean; domain: string; - tab: browser.Tabs.Tab | undefined; + tab?: browser.Tabs.Tab; blob: string; url: string; accountToSign: string; } +export interface EntryToSign { + isDomainListedAllowed: boolean; + domain: string; + tab?: browser.Tabs.Tab; + entry: string; + url: string; + accountToSign: string; +} + export const encodeObject = (obj: {}) => btoa(unescape(encodeURIComponent(JSON.stringify(obj)))); @@ -24,7 +33,7 @@ export const removeQueryParam = (url = "") => url.replace(/\?(.*)/, ""); export const parsedSearchParam = ( param: string, -): TransactionInfo | BlobToSign => { +): TransactionInfo | BlobToSign | EntryToSign => { const decodedSearchParam = decodeString(param.replace("?", "")); return decodedSearchParam ? JSON.parse(decodedSearchParam) : {}; }; diff --git a/extension/src/popup/Router.tsx b/extension/src/popup/Router.tsx index 3a3c1c49f3..c2639f6418 100644 --- a/extension/src/popup/Router.tsx +++ b/extension/src/popup/Router.tsx @@ -46,6 +46,7 @@ import { MnemonicPhrase } from "popup/views/MnemonicPhrase"; import { FullscreenSuccessMessage } from "popup/views/FullscreenSuccessMessage"; import { RecoverAccount } from "popup/views/RecoverAccount"; import { SignTransaction } from "popup/views/SignTransaction"; +import { SignAuthEntry } from "popup/views/SignAuthEntry"; import { UnlockAccount } from "popup/views/UnlockAccount"; import { Welcome } from "popup/views/Welcome"; import { DisplayBackupPhrase } from "popup/views/DisplayBackupPhrase"; @@ -65,6 +66,7 @@ import { PinExtension } from "popup/views/PinExtension"; import "popup/metrics/views"; import { DEV_SERVER } from "@shared/constants/services"; +import { SignBlob } from "./views/SignBlob"; import { SorobanProvider } from "./SorobanContext"; @@ -276,6 +278,12 @@ export const Router = () => { + + + + + + diff --git a/extension/src/popup/SorobanContext.tsx b/extension/src/popup/SorobanContext.tsx index 9677496388..f21afa95f7 100644 --- a/extension/src/popup/SorobanContext.tsx +++ b/extension/src/popup/SorobanContext.tsx @@ -2,16 +2,18 @@ import React from "react"; import { useSelector } from "react-redux"; import * as SorobanClient from "soroban-client"; -import { - SOROBAN_RPC_URLS, - FUTURENET_NETWORK_DETAILS, -} from "@shared/constants/stellar"; +import { SOROBAN_RPC_URLS, NETWORKS } from "@shared/constants/stellar"; import { settingsNetworkDetailsSelector } from "./ducks/settings"; +export const hasSorobanClient = ( + context: SorobanContextInterface, +): context is Required => + context.server !== undefined && context.newTxBuilder !== undefined; + export interface SorobanContextInterface { - server: SorobanClient.Server; - newTxBuilder: () => SorobanClient.TransactionBuilder; + server?: SorobanClient.Server; + newTxBuilder?: (fee?: string) => Promise; } export const SorobanContext = React.createContext( @@ -25,29 +27,43 @@ export const SorobanProvider = ({ children: React.ReactNode; pubKey: string; }) => { - // Were only simluating so the fee here should not matter - // AFAIK there is no fee stats for Soroban yet either - const fee = "100"; const networkDetails = useSelector(settingsNetworkDetailsSelector); - const source = new SorobanClient.Account(pubKey, "0"); - - const serverUrl = - networkDetails.networkPassphrase === - "Test SDF Future Network ; October 2022" && - networkDetails.networkUrl === FUTURENET_NETWORK_DETAILS.networkUrl - ? SOROBAN_RPC_URLS.FUTURENET - : networkDetails.networkUrl; - - const server = new SorobanClient.Server(serverUrl, { - allowHttp: networkDetails.networkUrl.startsWith("http://"), - }); - - const newTxBuilder = () => - new SorobanClient.TransactionBuilder(source, { - fee, - networkPassphrase: networkDetails.networkPassphrase, + + let server: SorobanContextInterface["server"]; + let newTxBuilder: SorobanContextInterface["newTxBuilder"]; + if ( + !networkDetails.sorobanRpcUrl && + networkDetails.network === NETWORKS.FUTURENET + ) { + // TODO: after enough time has passed to assume most clients have ran + // the migrateSorobanRpcUrlNetworkDetails migration, remove and use networkDetails.sorobanRpcUrl + const serverUrl = SOROBAN_RPC_URLS[NETWORKS.FUTURENET]!; + + server = new SorobanClient.Server(serverUrl, { + allowHttp: serverUrl.startsWith("http://"), }); + newTxBuilder = async (fee = SorobanClient.BASE_FEE) => { + const sourceAccount = await server!.getAccount(pubKey); + return new SorobanClient.TransactionBuilder(sourceAccount, { + fee, + networkPassphrase: networkDetails.networkPassphrase, + }); + }; + } else if (networkDetails.sorobanRpcUrl) { + server = new SorobanClient.Server(networkDetails.sorobanRpcUrl, { + allowHttp: networkDetails.sorobanRpcUrl.startsWith("http://"), + }); + + newTxBuilder = async (fee = SorobanClient.BASE_FEE) => { + const sourceAccount = await server!.getAccount(pubKey); + return new SorobanClient.TransactionBuilder(sourceAccount, { + fee, + networkPassphrase: networkDetails.networkPassphrase, + }); + }; + } + return ( {children} diff --git a/extension/src/popup/__testHelpers__/index.tsx b/extension/src/popup/__testHelpers__/index.tsx index 06f8dea0c6..80f774431d 100644 --- a/extension/src/popup/__testHelpers__/index.tsx +++ b/extension/src/popup/__testHelpers__/index.tsx @@ -1,5 +1,6 @@ import React from "react"; import { Provider } from "react-redux"; +import * as SorobanClient from "soroban-client"; import BigNumber from "bignumber.js"; import { createMemoryHistory } from "history"; import { @@ -8,7 +9,8 @@ import { getDefaultMiddleware, } from "@reduxjs/toolkit"; import { APPLICATION_STATE } from "@shared/constants/applicationState"; -import { ActionStatus, Balances } from "@shared/api/types"; +import { Balances } from "@shared/api/types"; +import { FUTURENET_NETWORK_DETAILS } from "@shared/constants/stellar"; import { isSerializable } from "helpers/stellar"; import { reducer as auth } from "popup/ducks/accountServices"; @@ -17,7 +19,11 @@ import { reducer as transactionSubmission, initialState as transactionSubmissionInitialState, } from "popup/ducks/transactionSubmission"; +import { initialState as sorobanInitialState } from "popup/ducks/soroban"; import { reducer as soroban } from "popup/ducks/soroban"; +import { SorobanContext } from "../SorobanContext"; + +const publicKey = "GA4UFF2WJM7KHHG4R5D5D2MZQ6FWMDOSVITVF7C5OLD5NFP6RBBW2FGV"; const rootReducer = combineReducers({ auth, @@ -41,6 +47,35 @@ const makeDummyStore = (state: any) => ], }); +const MockSorobanProvider = ({ + children, + pubKey, +}: { + children: React.ReactNode; + pubKey: string; +}) => { + const server = new SorobanClient.Server( + FUTURENET_NETWORK_DETAILS.networkUrl, + { + allowHttp: FUTURENET_NETWORK_DETAILS.networkUrl.startsWith("http://"), + }, + ); + + const newTxBuilder = async (fee = SorobanClient.BASE_FEE) => { + const sourceAccount = new SorobanClient.Account(pubKey, "0"); + return new SorobanClient.TransactionBuilder(sourceAccount, { + fee, + networkPassphrase: FUTURENET_NETWORK_DETAILS.networkPassphrase, + }); + }; + + return ( + + {children} + + ); +}; + export const Wrapper: React.FunctionComponent = ({ children, state, @@ -63,14 +98,13 @@ export const Wrapper: React.FunctionComponent = ({ applicationState: APPLICATION_STATE.MNEMONIC_PHRASE_CONFIRMED, }, transactionSubmission: transactionSubmissionInitialState, - soroban: { - getTokenBalancesStatus: ActionStatus.IDLE, - tokenBalances: [], - }, + soroban: sorobanInitialState, ...state, })} > - {children} + + {children} + @@ -99,6 +133,25 @@ export const mockBalances = { subentryCount: 1, }; +export const mockTokenBalances = { + tokenBalances: [ + { + contractId: "CCXVDIGMR6WTXZQX2OEVD6YM6AYCYPXPQ7YYH6OZMRS7U6VD3AVHNGBJ", + decimals: 0, + name: "Demo Token", + symbol: "DT", + total: new BigNumber(10), + }, + ], +}; + +export const mockTokenBalance = { + balance: 10, + decimals: 0, + name: "Demo Token", + symbol: "DT", +}; + export const mockAccounts = [ { hardwareWalletType: "", diff --git a/extension/src/popup/assets/illo-pin-extension.svg b/extension/src/popup/assets/illo-pin-extension.svg new file mode 100644 index 0000000000..df2173d34c --- /dev/null +++ b/extension/src/popup/assets/illo-pin-extension.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extension/src/popup/components/account/AccountAssets/index.tsx b/extension/src/popup/components/account/AccountAssets/index.tsx index 67e35a228f..088c290341 100644 --- a/extension/src/popup/components/account/AccountAssets/index.tsx +++ b/extension/src/popup/components/account/AccountAssets/index.tsx @@ -241,7 +241,7 @@ export const AccountAssets = ({
-
+
{formatAmount(amountVal)} {amountUnit}
diff --git a/extension/src/popup/components/accountHistory/HistoryItem/index.tsx b/extension/src/popup/components/accountHistory/HistoryItem/index.tsx index de3a538192..eb924861e1 100644 --- a/extension/src/popup/components/accountHistory/HistoryItem/index.tsx +++ b/extension/src/popup/components/accountHistory/HistoryItem/index.tsx @@ -1,17 +1,20 @@ -import React from "react"; +import React, { useContext, useState, useEffect } from "react"; +import { captureException } from "@sentry/browser"; import camelCase from "lodash/camelCase"; -import { Icon } from "@stellar/design-system"; +import { Icon, Loader } from "@stellar/design-system"; import { BigNumber } from "bignumber.js"; import { useTranslation } from "react-i18next"; import { OPERATION_TYPES } from "constants/transaction"; +import { SorobanTokenInterface } from "@shared/constants/soroban/token"; +import { getDecimals, getName, getSymbol } from "@shared/helpers/soroban/token"; import { METRIC_NAMES } from "popup/constants/metricsNames"; import { emitMetric } from "helpers/metrics"; +import { SorobanContext, hasSorobanClient } from "popup/SorobanContext"; import { formatTokenAmount, getAttrsFromSorobanHorizonOp, - SorobanTokenInterface, } from "popup/helpers/soroban"; import { formatAmount } from "popup/helpers/formatters"; @@ -21,6 +24,10 @@ import { NetworkDetails } from "@shared/constants/stellar"; import { TransactionDetailProps } from "../TransactionDetail"; import "./styles.scss"; +function capitalize(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + export const historyItemDetailViewProps: TransactionDetailProps = { operation: {} as HorizonOperation, headerTitle: "", @@ -59,6 +66,7 @@ export const HistoryItem = ({ setIsDetailViewShowing, }: HistoryItemProps) => { const { t } = useTranslation(); + const sorobanClient = useContext(SorobanContext); const { account, amount, @@ -80,7 +88,8 @@ export const HistoryItem = ({ sourceAssetCode = operation.source_asset_code; } const operationType = camelCase(type) as keyof typeof OPERATION_TYPES; - const operationString = `${OPERATION_TYPES[operationType]}${ + const opTypeStr = OPERATION_TYPES[operationType] || t("Transaction"); + const operationString = `${opTypeStr}${ operationCount > 1 ? ` + ${operationCount - 1} ops` : "" }`; const date = new Date(Date.parse(createdAt)) @@ -90,22 +99,12 @@ export const HistoryItem = ({ .join(" "); const srcAssetCode = sourceAssetCode || "XLM"; const destAssetCode = assetCode || "XLM"; + const isInvokeHostFn = typeI === 24; - let isRecipient = false; - let paymentDifference = ""; - let rowText = ""; - let dateText = date; - let IconComponent = ( - - ); - let PaymentComponent = null as React.ReactElement | null; - // TODO should be combined with isPayment - const isSorobanTx = typeI === 24; - - let transactionDetailProps: TransactionDetailProps = { + const transactionDetailPropsBase: TransactionDetailProps = { operation, isCreateExternalAccount, - isRecipient, + isRecipient: false, isPayment, isSwap, headerTitle: "", @@ -114,180 +113,351 @@ export const HistoryItem = ({ setIsDetailViewShowing, }; - if (isSwap) { - PaymentComponent = ( - <> - {new BigNumber(amount).toFixed(2, 1)} {destAssetCode} - - ); - rowText = t(`{{srcAssetCode}} for {{destAssetCode}}`, { - srcAssetCode, - destAssetCode, - }); - dateText = t(`Swap \u2022 {{date}}`, { date }); - transactionDetailProps = { - ...transactionDetailProps, - headerTitle: t(`Swapped {{srcAssetCode}} for {{destAssetCode}}`, { - srcAssetCode, - destAssetCode, - }), - operationText: `+${new BigNumber(amount)} ${destAssetCode}`, - }; - } else if (isPayment) { - // default to Sent if a payment to self - isRecipient = to === publicKey && from !== publicKey; - paymentDifference = isRecipient ? "+" : "-"; - PaymentComponent = ( - <> - {paymentDifference} - {formatAmount(new BigNumber(amount).toFixed(2, 1))} {destAssetCode} - - ); - IconComponent = isRecipient ? ( - - ) : ( - - ); - rowText = destAssetCode; - dateText = `${isRecipient ? t("Received") : t("Sent")} \u2022 ${date}`; - transactionDetailProps = { - ...transactionDetailProps, - isRecipient, - headerTitle: `${ - isRecipient ? t("Received") : t("Sent") - } ${destAssetCode}`, - operationText: `${paymentDifference}${new BigNumber( - amount, - )} ${destAssetCode}`, - }; - } else if (isCreateExternalAccount) { - PaymentComponent = <>-{new BigNumber(startingBalance).toFixed(2, 1)} XLM; - IconComponent = ; - rowText = "XLM"; - dateText = `${t("Sent")} \u2022 ${date}`; - transactionDetailProps = { - ...transactionDetailProps, - headerTitle: t("Create Account"), - isPayment: true, - operation: { - ...operation, - asset_type: "native", - to: account, - }, - operationText: `-${new BigNumber(startingBalance)} XLM`, - }; - } else if (isSorobanTx) { - const attrs = getAttrsFromSorobanHorizonOp(operation, networkDetails); - const token = tokenBalances.find( - (balance) => attrs && balance.contractId === attrs.contractId, - ); + const [txDetails, setTxDetails] = useState(transactionDetailPropsBase); + const [dateText, setDateText] = useState(date); + const [rowText, setRowText] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [IconComponent, setIconComponent] = useState( + ( + + ) as React.ReactElement | null, + ); + const [BodyComponent, setBodyComponent] = useState( + null as React.ReactElement | null, + ); + + const renderBodyComponent = () => BodyComponent; + const renderIcon = () => IconComponent; + + useEffect(() => { + const buildHistoryItem = async () => { + if (isSwap) { + setBodyComponent( + <> + {new BigNumber(amount).toFixed(2, 1)} {destAssetCode} + , + ); + setRowText( + t(`{{srcAssetCode}} for {{destAssetCode}}`, { + srcAssetCode, + destAssetCode, + }), + ); + setTxDetails((_state) => ({ + ..._state, + headerTitle: t(`Swapped {{srcAssetCode}} for {{destAssetCode}}`, { + srcAssetCode, + destAssetCode, + }), + operationText: `+${new BigNumber(amount)} ${destAssetCode}`, + })); + } else if (isPayment) { + // default to Sent if a payment to self + const _isRecipient = to === publicKey && from !== publicKey; + const paymentDifference = _isRecipient ? "+" : "-"; + setBodyComponent( + <> + {paymentDifference} + {formatAmount(new BigNumber(amount).toFixed(2, 1))} {destAssetCode} + , + ); + setIconComponent( + _isRecipient ? ( + + ) : ( + + ), + ); + setRowText(destAssetCode); + setDateText( + (_dateText) => + `${_isRecipient ? t("Received") : t("Sent")} \u2022 ${date}`, + ); + setTxDetails((_state) => ({ + ..._state, + isRecipient: _isRecipient, + headerTitle: `${ + _isRecipient ? t("Received") : t("Sent") + } ${destAssetCode}`, + operationText: `${paymentDifference}${new BigNumber( + amount, + )} ${destAssetCode}`, + })); + } else if (isCreateExternalAccount) { + setBodyComponent( + <>-{new BigNumber(startingBalance).toFixed(2, 1)} XLM, + ); + setIconComponent(); + setRowText("XLM"); + setDateText((_dateText) => `${t("Sent")} \u2022 ${date}`); + setTxDetails((_state) => ({ + ..._state, + headerTitle: t("Create Account"), + isPayment: true, + operation: { + ...operation, + asset_type: "native", + to: account, + }, + operationText: `-${new BigNumber(startingBalance)} XLM`, + })); + } else if (isInvokeHostFn) { + const attrs = getAttrsFromSorobanHorizonOp(operation, networkDetails); + const token = tokenBalances.find( + (balance) => attrs && balance.contractId === attrs.contractId, + ); + + if (!attrs) { + setRowText(operationString); + setTxDetails((_state) => ({ + ..._state, + headerTitle: t("Transaction"), + operationText: operationString, + })); + } else if (attrs.fnName === SorobanTokenInterface.mint) { + const isRecieving = attrs.to === publicKey; + + setIconComponent( + isRecieving ? ( + + ) : ( + + ), + ); + + // Minter does not need to have tokens to mint, and + // they are not neccessarily minted to themselves. + // If user has minted to self, add token to their token list. + if (!token) { + setIsLoading(true); + // TODO: When fetching contract details, we could encounter an expired state entry + // and fail to fetch values through the RPC. + // We can address this in several ways - + // 1. If token is a SAC, fetch details from Horizon. + // 2. If not SAC or unknown, look up ledger entry directly. - if (!token || !attrs) { - rowText = operationString; - transactionDetailProps = { - ...transactionDetailProps, - headerTitle: t("Transaction"), - operationText: operationString, - }; - } else if (attrs.fnName === SorobanTokenInterface.mint) { - // handle a mint operation, which is similar to a Sent Payment, but with subtle differences - const formattedTokenAmount = formatTokenAmount( - new BigNumber(attrs.amount), - Number(token.decimals), - ); - PaymentComponent = ( - <> - {formattedTokenAmount} {token.symbol} - - ); - IconComponent = ; + try { + if (!hasSorobanClient(sorobanClient)) { + throw new Error("Soroban RPC not supported for this network"); + } - // specify that this is a mint operation - dateText = `${t("Mint")} \u2022 ${date}`; - rowText = token.symbol; - transactionDetailProps = { - ...transactionDetailProps, - operation: { - ...transactionDetailProps.operation, - from: attrs.from, - to: attrs.to, - }, - // we don't use a +/- as mint does not negatively affect balance - headerTitle: `${t("Mint")} ${token.symbol}`, - // manually set `isPayment` now that we've passed the above `isPayment` conditional - isPayment: true, - isRecipient: false, - operationText: `${formattedTokenAmount} ${token.symbol}`, - }; - } else { - // otherwise handle as a token payment - const formattedTokenAmount = formatTokenAmount( - new BigNumber(attrs.amount), - Number(token.decimals), - ); + const tokenDecimals = await getDecimals( + attrs.contractId, + sorobanClient.server, + await sorobanClient.newTxBuilder(), + ); + const tokenName = await getName( + attrs.contractId, + sorobanClient.server, + await sorobanClient.newTxBuilder(), + ); + const tokenSymbol = await getSymbol( + attrs.contractId, + sorobanClient.server, + await sorobanClient.newTxBuilder(), + ); - // we're not getting token received ops from Horizon, - // but we'll check to make sure we're not the one sending the payment - isRecipient = attrs.from !== publicKey; - paymentDifference = isRecipient ? "+" : "-"; - PaymentComponent = ( - <> - {paymentDifference} - {formattedTokenAmount} {token.symbol} - - ); - IconComponent = isRecipient ? ( - - ) : ( - - ); - dateText = `${isRecipient ? t("Received") : t("Sent")} \u2022 ${date}`; - rowText = token.symbol; - transactionDetailProps = { - ...transactionDetailProps, - operation: { - ...transactionDetailProps.operation, - from: attrs.from, - to: attrs.to, - }, - // manually set `isPayment` now that we've passed the above `isPayment` conditional - isPayment: true, - isRecipient, - headerTitle: `${isRecipient ? t("Received") : t("Sent")} ${ - token.symbol - }`, - operationText: `${paymentDifference}${formattedTokenAmount} ${token.symbol}`, - }; - } - } else { - rowText = operationString; - transactionDetailProps = { - ...transactionDetailProps, - headerTitle: t("Transaction"), - operationText: operationString, + const _token = { + contractId: attrs.contractId, + total: isRecieving ? attrs.amount : 0, + decimals: tokenDecimals, + name: tokenName, + symbol: tokenSymbol, + }; + + const formattedTokenAmount = formatTokenAmount( + new BigNumber(attrs.amount), + _token.decimals, + ); + setBodyComponent( + <> + {isRecieving && "+"} + {formattedTokenAmount} {_token.symbol} + , + ); + + setDateText( + (_dateText) => + `${isRecieving ? t("Received") : t("Minted")} \u2022 ${date}`, + ); + setRowText(t(capitalize(attrs.fnName))); + setTxDetails((_state) => ({ + ..._state, + operation: { + ..._state.operation, + from: attrs.from, + to: attrs.to, + }, + headerTitle: `${t(capitalize(attrs.fnName))} ${tokenSymbol}`, + isPayment: false, + isRecipient: isRecieving, + operationText: `${formattedTokenAmount} ${tokenSymbol}`, + })); + setIsLoading(false); + } catch (error) { + console.error(error); + captureException(`Error fetching token details: ${error}`); + setRowText(t(capitalize(attrs.fnName))); + setBodyComponent( + <> + {isRecieving && "+ "} + Unknown + , + ); + setDateText( + (_dateText) => + `${isRecieving ? t("Received") : t("Minted")} \u2022 ${date}`, + ); + setTxDetails((_state) => ({ + ..._state, + operation: { + ..._state.operation, + from: attrs.from, + to: attrs.to, + }, + headerTitle: t(capitalize(attrs.fnName)), + // manually set `isPayment` now that we've passed the above `isPayment` conditional + isPayment: false, + isRecipient: isRecieving, + operationText: operationString, + })); + setIsLoading(false); + } + } else { + const formattedTokenAmount = formatTokenAmount( + new BigNumber(attrs.amount), + token.decimals, + ); + setBodyComponent( + <> + {isRecieving && "+"} + {formattedTokenAmount} {token.symbol} + , + ); + + setDateText( + (_dateText) => + `${isRecieving ? t("Received") : t("Minted")} \u2022 ${date}`, + ); + setRowText(t(capitalize(attrs.fnName))); + setTxDetails((_state) => ({ + ..._state, + operation: { + ..._state.operation, + from: attrs.from, + to: attrs.to, + }, + headerTitle: `${t(capitalize(attrs.fnName))} ${token.symbol}`, + isPayment: false, + isRecipient: isRecieving, + operationText: `${formattedTokenAmount} ${token.symbol}`, + })); + } + } else if (attrs.fnName === SorobanTokenInterface.transfer) { + setIconComponent( + , + ); + + if (!token) { + // this should never happen, transfers cant succeed if you have no balance. + setRowText(operationString); + setTxDetails((_state) => ({ + ..._state, + headerTitle: t("Transaction"), + operationText: operationString, + })); + } else { + const formattedTokenAmount = formatTokenAmount( + new BigNumber(attrs.amount), + token.decimals, + ); + setBodyComponent( + <> + - {formattedTokenAmount} {token.symbol} + , + ); + + setDateText((_dateText) => `${t("Sent")} \u2022 ${date}`); + setRowText(t(capitalize(attrs.fnName))); + setTxDetails((_state) => ({ + ..._state, + operation: { + ..._state.operation, + from: attrs.from, + to: attrs.to, + }, + headerTitle: `${t(capitalize(attrs.fnName))} ${token.symbol}`, + isPayment: false, + isRecipient: false, + operationText: `${formattedTokenAmount} ${token.symbol}`, + })); + } + } else { + setRowText(operationString); + setTxDetails((_state) => ({ + ..._state, + headerTitle: t("Transaction"), + operationText: operationString, + })); + } + } else { + setRowText(operationString); + setTxDetails((_state) => ({ + ..._state, + headerTitle: t("Transaction"), + operationText: operationString, + })); + } }; - } - const renderPaymentComponent = () => PaymentComponent; - const renderIcon = () => IconComponent; + buildHistoryItem(); + }, [ + account, + amount, + date, + destAssetCode, + from, + isCreateExternalAccount, + isInvokeHostFn, + isPayment, + isSwap, + networkDetails, + operation, + operationString, + publicKey, + sorobanClient, + srcAssetCode, + startingBalance, + t, + to, + tokenBalances, + ]); return (
{ emitMetric(METRIC_NAMES.historyOpenItem); - setDetailViewProps(transactionDetailProps); + setDetailViewProps(txDetails); setIsDetailViewShowing(true); }} >
-
{renderIcon()}
-
- {rowText} -
{dateText}
-
+ {isLoading ? ( +
+ +
+ ) : ( + <> +
{renderIcon()}
+
+ {rowText} +
{dateText}
+
-
{renderPaymentComponent()}
+
{renderBodyComponent()}
+ + )}
); diff --git a/extension/src/popup/components/accountHistory/HistoryItem/styles.scss b/extension/src/popup/components/accountHistory/HistoryItem/styles.scss index 5e8b47b897..f84f2b30cb 100644 --- a/extension/src/popup/components/accountHistory/HistoryItem/styles.scss +++ b/extension/src/popup/components/accountHistory/HistoryItem/styles.scss @@ -2,6 +2,13 @@ color: var(--color-gray-90); cursor: pointer; + &__loader { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + &__row { display: flex; } diff --git a/extension/src/popup/components/manageAssets/AddToken/index.tsx b/extension/src/popup/components/manageAssets/AddToken/index.tsx index bab685fca2..2e70c56c37 100644 --- a/extension/src/popup/components/manageAssets/AddToken/index.tsx +++ b/extension/src/popup/components/manageAssets/AddToken/index.tsx @@ -3,6 +3,7 @@ import { useDispatch, useSelector } from "react-redux"; import { Button, Input } from "@stellar/design-system"; import { Field, Form, Formik, FieldProps } from "formik"; import { useTranslation } from "react-i18next"; +import { Networks } from "soroban-client"; import { ROUTES } from "popup/constants/routes"; import { METRIC_NAMES } from "popup/constants/metricsNames"; @@ -16,6 +17,7 @@ import { PopupWrapper } from "popup/basics/PopupWrapper"; import { SubviewHeader } from "popup/components/SubviewHeader"; import { addTokenId, authErrorSelector } from "popup/ducks/accountServices"; +import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; interface FormValues { tokenId: string; @@ -29,10 +31,13 @@ export const AddToken = () => { const { t } = useTranslation(); const dispatch: AppDispatch = useDispatch(); const authError = useSelector(authErrorSelector); + const networkDetails = useSelector(settingsNetworkDetailsSelector); const handleSubmit = async (values: FormValues) => { const { tokenId } = values; - const res = await dispatch(addTokenId(tokenId)); + const res = await dispatch( + addTokenId({ tokenId, network: networkDetails.network as Networks }), + ); if (addTokenId.fulfilled.match(res)) { emitMetric(METRIC_NAMES.manageAssetAddToken); diff --git a/extension/src/popup/components/manageNetwork/NetworkForm/index.tsx b/extension/src/popup/components/manageNetwork/NetworkForm/index.tsx index 14f15d66a6..43ab6ec176 100644 --- a/extension/src/popup/components/manageNetwork/NetworkForm/index.tsx +++ b/extension/src/popup/components/manageNetwork/NetworkForm/index.tsx @@ -10,6 +10,11 @@ import { AppDispatch } from "popup/App"; import { SimpleBarWrapper } from "popup/basics/SimpleBarWrapper"; import { PillButton } from "popup/basics/buttons/PillButton"; import { ROUTES } from "popup/constants/routes"; +import { + NETWORKS, + NETWORK_NAMES, + SOROBAN_RPC_URLS, +} from "@shared/constants/stellar"; import { navigateTo } from "popup/helpers/navigate"; import { isNetworkUrlValid as isNetworkUrlValidHelper } from "popup/helpers/account"; @@ -36,6 +41,7 @@ interface FormValues { networkName: string; networkPassphrase: string; networkUrl: string; + sorobanRpcUrl?: string; isSwitchSelected?: boolean; isAllowHttpSelected: boolean; friendbotUrl?: string; @@ -76,6 +82,8 @@ export const NetworkForm = ({ isEditing }: NetworkFormProps) => { ? { ...networkDetailsToEdit, isSwitchSelected: false, + sorobanRpcUrl: + SOROBAN_RPC_URLS[networkDetailsToEdit.network as NETWORKS], isAllowHttpSelected: !networkDetailsToEdit?.networkUrl.includes( "https", ), @@ -84,6 +92,7 @@ export const NetworkForm = ({ isEditing }: NetworkFormProps) => { networkName: "", networkPassphrase: "", networkUrl: "", + sorobanRpcUrl: "", friendbotUrl: "", isSwitchSelected: false, isAllowHttpSelected: false, @@ -93,6 +102,7 @@ export const NetworkForm = ({ isEditing }: NetworkFormProps) => { networkName: YupString().required(), networkPassphrase: YupString().required(), networkUrl: YupString().required(), + sorobanRpcUrl: YupString(), }); const handleRemoveNetwork = async () => { @@ -113,7 +123,13 @@ export const NetworkForm = ({ isEditing }: NetworkFormProps) => { }; const getCustomNetworkDetailsFromFormValues = (values: FormValues) => { - const { friendbotUrl, networkName, networkUrl, networkPassphrase } = values; + const { + friendbotUrl, + networkName, + networkUrl, + networkPassphrase, + sorobanRpcUrl, + } = values; return { friendbotUrl, @@ -121,6 +137,7 @@ export const NetworkForm = ({ isEditing }: NetworkFormProps) => { networkName, networkUrl, networkPassphrase, + sorobanRpcUrl, }; }; @@ -179,6 +196,9 @@ export const NetworkForm = ({ isEditing }: NetworkFormProps) => { } }; + const supportsSorobanRpc = (network: string) => + network === NETWORK_NAMES.FUTURENET; + const handleSubmit = async (values: FormValues) => { if (isEditing) { await handleEditNetwork(values); @@ -345,10 +365,28 @@ export const NetworkForm = ({ isEditing }: NetworkFormProps) => { : "" } customInput={} - label={t("URL")} + label={t("HORIZON RPC URL")} name="networkUrl" placeholder={t("Enter network URL")} /> + {supportsSorobanRpc(initialValues.networkName) || + !isEditingDefaultNetworks ? ( + } + label={t("SOROBAN RPC URL")} + name="sorobanRpcUrl" + placeholder={t("Enter Soroban RPC URL")} + /> + ) : null} void }) => { const { t } = useTranslation(); - const { server: sorobanServer } = useContext(SorobanContext); - const publicKey = useSelector(publicKeySelector); const networkDetails = useSelector(settingsNetworkDetailsSelector); const hardwareWalletType = useSelector(hardwareWalletTypeSelector); @@ -280,33 +278,24 @@ export const TransactionDetails = ({ goBack }: { goBack: () => void }) => { Number(assetBalance.decimals), ); - const sourceAccount = await sorobanServer.getAccount(publicKey); - const contract = new SorobanClient.Contract(assetAddress); - const contractOp = contract.call( - "transfer", - ...[ - accountIdentifier(publicKey), // from - accountIdentifier(destination), // to - numberToI128(parsedAmount.toNumber()), // amount - ], - ); - - const transaction = await new SorobanClient.TransactionBuilder( - sourceAccount, - { - fee: xlmToStroop(transactionFee).toFixed(), - networkPassphrase: networkDetails.networkPassphrase, - }, - ) - .addOperation(contractOp) - .setTimeout(180); + const params = [ + new SorobanClient.Address(publicKey).toScVal(), // from + new SorobanClient.Address(destination).toScVal(), // to + new SorobanClient.XdrLargeInt("i128", parsedAmount.toNumber()).toI128(), // amount + ]; - if (memo) { - transaction.addMemo(SorobanClient.Memo.text(memo)); + if (!hasSorobanClient(sorobanClient)) { + throw new Error("Soroban RPC not supported for this network"); } - const preparedTransaction = await sorobanServer.prepareTransaction( - transaction.build(), + const builder = await sorobanClient.newTxBuilder( + xlmToStroop(transactionFee).toFixed(), + ); + + const transaction = await transfer(assetAddress, params, memo, builder); + + const preparedTransaction = await sorobanClient.server.prepareTransaction( + transaction, networkDetails.networkPassphrase, ); diff --git a/extension/src/popup/components/signAuthEntry/AuthEntry/index.tsx b/extension/src/popup/components/signAuthEntry/AuthEntry/index.tsx new file mode 100644 index 0000000000..3094bfcd26 --- /dev/null +++ b/extension/src/popup/components/signAuthEntry/AuthEntry/index.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { xdr } from "soroban-client"; + +import { SimpleBarWrapper } from "popup/basics/SimpleBarWrapper"; +import { buildInvocationTree } from "../invocation"; +import "./styles.scss"; + +interface TransactionProps { + authEntryXdr: string; +} + +export const AuthEntry = ({ authEntryXdr }: TransactionProps) => { + const { t } = useTranslation(); + const authEntry = xdr.SorobanAuthorizationEntry.fromXDR( + authEntryXdr, + "base64", + ); + const rootJson = buildInvocationTree(authEntry.rootInvocation()); + + return ( +
+
{t("Authorization Entry")}
+
+
+          
+            {JSON.stringify(rootJson, null, 2)}
+          
+        
+
+
+ ); +}; diff --git a/extension/src/popup/components/signAuthEntry/AuthEntry/styles.scss b/extension/src/popup/components/signAuthEntry/AuthEntry/styles.scss new file mode 100644 index 0000000000..77aa5d1559 --- /dev/null +++ b/extension/src/popup/components/signAuthEntry/AuthEntry/styles.scss @@ -0,0 +1,26 @@ +.AuthEntry { + .AuthEntryHeader { + border-bottom: 1px solid var(--pal-border-secondary); + font-size: 0.875rem; + font-weight: var(--font-weight-medium); + line-height: 0.72rem; + margin-bottom: 1rem; + padding-bottom: 1rem; + text-transform: uppercase; + } + + .AuthEntryAttributes { + display: flex; + flex-direction: column; + gap: 2rem; + margin-bottom: 1.5rem; + + pre { + background: var(--pal-example-details); + border-radius: 0.5rem; + font-size: 0.875rem; + margin: 0.5rem 0; + padding: 1rem; + } + } +} diff --git a/extension/src/popup/components/signAuthEntry/invocation.ts b/extension/src/popup/components/signAuthEntry/invocation.ts new file mode 100644 index 0000000000..e815b73d50 --- /dev/null +++ b/extension/src/popup/components/signAuthEntry/invocation.ts @@ -0,0 +1,87 @@ +import { Asset, Address, scValToNative, xdr } from "soroban-client"; + +interface RootOutput { + type: string; + args: Record; + subInvocations: RootOutput[]; +} + +export function buildInvocationTree(root: xdr.SorobanAuthorizedInvocation) { + const fn = root.function(); + const output = {} as RootOutput; + + switch (fn.switch().value) { + // sorobanAuthorizedFunctionTypeContractFn + case 0: { + const inner = fn.value() as xdr.InvokeContractArgs; + output.type = "execute"; + output.args = { + source: Address.fromScAddress(inner.contractAddress()).toString(), + function: inner.functionName(), + args: inner.args().map((arg) => scValToNative(arg).toString()), + }; + break; + } + + // sorobanAuthorizedFunctionTypeCreateContractHostFn + case 1: { + const inner = fn.value() as xdr.CreateContractArgs; + output.type = "create"; + output.args = { + type: "sac", + }; + + // If the executable is a WASM, the preimage MUST be an address. If it's a + // token, the preimage MUST be an asset. This is a cheeky way to check + // that, because wasm=0, address=1 and token=1, asset=0 in the XDR switch + // values. + // + // The first part may not be true in V2, but we'd need to update this code + // anyway so it can still be an error. + const [exec, preimage] = [inner.executable(), inner.contractIdPreimage()]; + if (!exec.switch().value !== !!preimage.switch().value) { + throw new Error( + `creation function appears invalid: ${JSON.stringify(inner)}`, + ); + } + + switch (exec.switch().value) { + // contractExecutableWasm + case 0: { + const details = preimage.fromAddress(); + + output.args.type = "wasm"; + output.args.args = { + hash: exec.wasmHash().toString("hex"), + address: Address.fromScAddress(details.address()).toString(), + salt: details.salt().toString("hex"), + }; + break; + } + + // contractExecutableToken + case 1: + output.args.type = "sac"; + output.args.asset = Asset.fromOperation( + preimage.fromAsset(), + ).toString(); + break; + + default: + throw new Error(`unknown creation type: ${JSON.stringify(exec)}`); + } + + break; + } + + default: + throw new Error( + `unknown invocation type (${fn.switch()}): ${JSON.stringify(fn)}`, + ); + } + + output.subInvocations = root + .subInvocations() + .map((i) => buildInvocationTree(i)); + return output; +} diff --git a/extension/src/popup/components/signBlob/index.tsx b/extension/src/popup/components/signBlob/index.tsx index ec422b050d..1d3c2ecd0d 100644 --- a/extension/src/popup/components/signBlob/index.tsx +++ b/extension/src/popup/components/signBlob/index.tsx @@ -1,4 +1,5 @@ import React from "react"; +import * as Sentry from "@sentry/browser"; import { Card, Heading } from "@stellar/design-system"; import "./index.scss"; @@ -7,11 +8,23 @@ interface BlobProps { blob: string; } -export const Blob = (props: BlobProps) => ( - - - Signing data: - -
{props.blob}
-
-); +export const Blob = (props: BlobProps) => { + let displayBlob = props.blob; + + try { + displayBlob = atob(props.blob); + } catch (error) { + Sentry.captureException( + `Failed to convert blob to display - ${props.blob}`, + ); + } + + return ( + + + Signing data: + +
{displayBlob}
+
+ ); +}; diff --git a/extension/src/popup/components/signTransaction/Operations/index.tsx b/extension/src/popup/components/signTransaction/Operations/index.tsx index e5d57bd2a8..ccec9ced7c 100644 --- a/extension/src/popup/components/signTransaction/Operations/index.tsx +++ b/extension/src/popup/components/signTransaction/Operations/index.tsx @@ -10,6 +10,8 @@ import { TRANSACTION_WARNING, } from "constants/transaction"; +import { getDecimals } from "@shared/helpers/soroban/token"; + import { FlaggedKeys } from "types/transactions"; import { @@ -18,7 +20,6 @@ import { truncateString, } from "helpers/stellar"; import { - getContractDecimals, getAttrsFromSorobanTxOp, formatTokenAmount, } from "popup/helpers/soroban"; @@ -26,9 +27,11 @@ import { import { SimpleBarWrapper } from "popup/basics/SimpleBarWrapper"; import { KeyIdenticon } from "popup/components/identicons/KeyIdenticon"; -import { SorobanContext } from "popup/SorobanContext"; +import { hasSorobanClient, SorobanContext } from "popup/SorobanContext"; import "./styles.scss"; +import { xdr } from "soroban-client"; +import { buildInvocationTree } from "popup/components/signAuthEntry/invocation"; interface Path { code: string; @@ -165,6 +168,37 @@ const KeyValueWithScValue = ({
); +const KeyValueWithScAuth = ({ + operationKey, + operationValue, +}: { + operationKey: string; + operationValue: { + _attributes: { + credentials: xdr.SorobanCredentials; + rootInvocation: xdr.SorobanAuthorizedInvocation; + }; + }[]; +}) => { + // TODO: use getters in signTx to get these correctly + const rawEntry = operationValue[0] && operationValue[0]._attributes; + const authEntry = new xdr.SorobanAuthorizationEntry(rawEntry); + const rootJson = buildInvocationTree(authEntry.rootInvocation()); + return ( +
+
+ {operationKey} + {operationKey ? ":" : null} +
+ +
+
{JSON.stringify(rootJson, null, 2)}
+
+
+
+ ); +}; + const PathList = ({ paths }: { paths: [Path] }) => { const { t } = useTranslation(); @@ -320,11 +354,12 @@ export const Operations = ({ const [decimals, setDecimals] = useState(0); useEffect(() => { - if (!contractId) return; + if (!contractId || !hasSorobanClient(sorobanClient)) return; const fetchContractDecimals = async () => { - const contractDecimals = await getContractDecimals( - sorobanClient, + const contractDecimals = await getDecimals( contractId, + sorobanClient.server, + await sorobanClient.newTxBuilder(), ); setDecimals(contractDecimals); }; @@ -781,15 +816,9 @@ export const Operations = ({ /> ))} - {scFunc ? ( - - ) : null} {scAuth ? ( - ) : null} diff --git a/extension/src/popup/constants/metricsNames.ts b/extension/src/popup/constants/metricsNames.ts index b5addf81a9..fd7c93970a 100644 --- a/extension/src/popup/constants/metricsNames.ts +++ b/extension/src/popup/constants/metricsNames.ts @@ -19,6 +19,8 @@ export const METRIC_NAMES = { viewRecoverAccount: "loaded screen: recover account", viewRecoverAccountSuccess: "loaded screen: recover account: success", viewSignTransaction: "loaded screen: sign transaction", + viewSignBlob: "loaded screen: sign blob", + viewSignAuthEntry: "loaded screen: sign auth entry", viewUnlockAccount: "loaded screen: unlock account", viewVerifyAccount: "loaded screen: verify account", viewUnlockBackupPhrase: "loaded screen: unlock backup phrase", @@ -116,6 +118,9 @@ export const METRIC_NAMES = { signBlob: "sign blob: confirmed", rejectBlob: "sign blob: rejected", + signAuthEntry: "sign auth entry: confirmed", + rejectAuthEntry: "sign auth entry: rejected", + backupPhraseSuccess: "backup phrase: success", backupPhraseFail: "backup phrase: error", diff --git a/extension/src/popup/constants/routes.ts b/extension/src/popup/constants/routes.ts index a31b26362d..c5356ae2de 100644 --- a/extension/src/popup/constants/routes.ts +++ b/extension/src/popup/constants/routes.ts @@ -25,6 +25,8 @@ export enum ROUTES { swapConfirm = "/swap/confirm", addAccount = "/add-account", signTransaction = "/sign-transaction", + signBlob = "/sign-blob", + signAuthEntry = "/sign-auth-entry", grantAccess = "/grant-access", mnemonicPhrase = "/mnemonic-phrase", mnemonicPhraseConfirm = "/mnemonic-phrase/confirm", diff --git a/extension/src/popup/ducks/access.ts b/extension/src/popup/ducks/access.ts index 740492b586..f763fed89d 100644 --- a/extension/src/popup/ducks/access.ts +++ b/extension/src/popup/ducks/access.ts @@ -5,6 +5,7 @@ import { grantAccess as internalGrantAccess, signTransaction as internalSignTransaction, signBlob as internalSignBlob, + signAuthEntry as internalSignAuthEntry, } from "@shared/api/internal"; export const grantAccess = createAsyncThunk("grantAccess", internalGrantAccess); @@ -20,6 +21,7 @@ export const signTransaction = createAsyncThunk( ); export const signBlob = createAsyncThunk("signBlob", internalSignBlob); +export const signEntry = createAsyncThunk("signEntry", internalSignAuthEntry); // Basically an alias for metrics purposes export const rejectTransaction = createAsyncThunk( @@ -29,3 +31,7 @@ export const rejectTransaction = createAsyncThunk( // Basically an alias for metrics purposes export const rejectBlob = createAsyncThunk("rejectBlob", internalRejectAccess); +export const rejectAuthEntry = createAsyncThunk( + "rejectAuthEntry", + internalRejectAccess, +); diff --git a/extension/src/popup/ducks/accountServices.ts b/extension/src/popup/ducks/accountServices.ts index d4c2f12fd4..cf36cc3e0f 100644 --- a/extension/src/popup/ducks/accountServices.ts +++ b/extension/src/popup/ducks/accountServices.ts @@ -4,6 +4,8 @@ import { createSlice, } from "@reduxjs/toolkit"; import * as Sentry from "@sentry/browser"; +import { Networks } from "soroban-client"; + import { APPLICATION_STATE } from "@shared/constants/applicationState"; import { addAccount as addAccountService, @@ -342,15 +344,15 @@ export const signOut = createAsyncThunk< export const addTokenId = createAsyncThunk< { tokenIdList: string[] }, - string, + { tokenId: string; network: Networks }, { rejectValue: ErrorMessage } ->("auth/addToken", async (tokenId, thunkApi) => { +>("auth/addToken", async ({ tokenId, network }, thunkApi) => { let res = { tokenIdList: [] as string[], }; try { - res = await addTokenIdService(tokenId); + res = await addTokenIdService(tokenId, network); } catch (e) { console.error("Failed when adding a token: ", e.message); return thunkApi.rejectWithValue({ diff --git a/extension/src/popup/ducks/settings.ts b/extension/src/popup/ducks/settings.ts index eebbca4bbd..5af870dd7d 100644 --- a/extension/src/popup/ducks/settings.ts +++ b/extension/src/popup/ducks/settings.ts @@ -34,6 +34,7 @@ const initialState: Settings = { networkName: "", networkUrl: "", networkPassphrase: "", + sorobanRpcUrl: "", } as NetworkDetails, networksList: DEFAULT_NETWORKS, isMemoValidationEnabled: true, diff --git a/extension/src/popup/ducks/soroban.ts b/extension/src/popup/ducks/soroban.ts index 9067523567..7c6b3ce5e2 100644 --- a/extension/src/popup/ducks/soroban.ts +++ b/extension/src/popup/ducks/soroban.ts @@ -1,3 +1,4 @@ +import { Address, Networks } from "soroban-client"; import BigNumber from "bignumber.js"; import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; @@ -7,19 +8,25 @@ import { getTokenIds as internalGetTokenIds, } from "@shared/api/internal"; import { ErrorMessage, ActionStatus, TokenBalances } from "@shared/api/types"; -import { accountIdentifier } from "@shared/api/helpers/soroban"; -import { SorobanContextInterface } from "popup/SorobanContext"; +import { + SorobanContextInterface, + hasSorobanClient, +} from "popup/SorobanContext"; export const getTokenBalances = createAsyncThunk< TokenBalances, - { sorobanClient: SorobanContextInterface }, + { sorobanClient: SorobanContextInterface; network: Networks }, { rejectValue: ErrorMessage } ->("getTokenBalances", async ({ sorobanClient }, thunkApi) => { +>("getTokenBalances", async ({ sorobanClient, network }, thunkApi) => { + if (!sorobanClient.server || !sorobanClient.newTxBuilder) { + throw new Error("soroban rpc not supported"); + } + try { const { publicKey } = await internalLoadAccount(); - const tokenIdList = await internalGetTokenIds(); + const tokenIdList = await internalGetTokenIds(network); - const params = [accountIdentifier(publicKey)]; + const params = [new Address(publicKey).toScVal()]; const results = [] as TokenBalances; for (let i = 0; i < tokenIdList.length; i += 1) { @@ -27,25 +34,30 @@ export const getTokenBalances = createAsyncThunk< /* Right now, Soroban transactions only support 1 operation per tx so we need a builder per value from the contract, - once multi-op transactions are supported this can send + once/if multi-op transactions are supported this can send 1 tx with an operation for each value. */ try { - // eslint-disable-next-line no-await-in-loop + if (!hasSorobanClient(sorobanClient)) { + throw new Error("Soroban RPC is not supprted for this network"); + } + + /* eslint-disable no-await-in-loop */ const { balance, ...rest } = await internalGetSorobanTokenBalance( sorobanClient.server, tokenId, { - balance: sorobanClient.newTxBuilder(), - name: sorobanClient.newTxBuilder(), - decimals: sorobanClient.newTxBuilder(), - symbol: sorobanClient.newTxBuilder(), + balance: await sorobanClient.newTxBuilder(), + name: await sorobanClient.newTxBuilder(), + decimals: await sorobanClient.newTxBuilder(), + symbol: await sorobanClient.newTxBuilder(), }, params, ); + /* eslint-enable no-await-in-loop */ - const total = new BigNumber(balance) as any; // ?? why can't the BigNumber type work here + const total = new BigNumber(balance); results.push({ contractId: tokenId, @@ -64,7 +76,7 @@ export const getTokenBalances = createAsyncThunk< } }); -const initialState = { +export const initialState = { getTokenBalancesStatus: ActionStatus.IDLE, tokenBalances: [] as TokenBalances, }; diff --git a/extension/src/popup/ducks/transactionSubmission.ts b/extension/src/popup/ducks/transactionSubmission.ts index 9941b60e96..34ddead4be 100644 --- a/extension/src/popup/ducks/transactionSubmission.ts +++ b/extension/src/popup/ducks/transactionSubmission.ts @@ -1,4 +1,5 @@ import StellarSdk, { Horizon, Server, ServerApi } from "stellar-sdk"; +import { Networks, SorobanRpc } from "soroban-client"; import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import { @@ -109,7 +110,7 @@ export const submitFreighterTransaction = createAsyncThunk< ); export const submitFreighterSorobanTransaction = createAsyncThunk< - Horizon.TransactionResponse, + SorobanRpc.SendTransactionResponse, { signedXDR: string; networkDetails: NetworkDetails; @@ -133,7 +134,12 @@ export const submitFreighterSorobanTransaction = createAsyncThunk< if (refreshBalances) { thunkApi.dispatch(resetSorobanTokensStatus()); - await thunkApi.dispatch(getTokenBalances({ sorobanClient })); + await thunkApi.dispatch( + getTokenBalances({ + sorobanClient, + network: networkDetails.network as Networks, + }), + ); } return txRes; @@ -396,7 +402,10 @@ interface InitialState { submitStatus: ActionStatus; accountBalanceStatus: ActionStatus; hardwareWalletData: HardwareWalletData; - response: Horizon.TransactionResponse | null; + response: + | Horizon.TransactionResponse + | SorobanRpc.SendTransactionResponse + | null; error: ErrorMessage | undefined; transactionData: TransactionData; accountBalances: AccountBalancesInterface; diff --git a/extension/src/popup/helpers/account.ts b/extension/src/popup/helpers/account.ts index 01079326fa..820aea1241 100644 --- a/extension/src/popup/helpers/account.ts +++ b/extension/src/popup/helpers/account.ts @@ -8,13 +8,14 @@ import { SorobanBalance, } from "@shared/api/types"; import { NetworkDetails } from "@shared/constants/stellar"; +import { SorobanTokenInterface } from "@shared/constants/soroban/token"; import { getAssetFromCanonical, getCanonicalFromAsset, isTestnet, } from "helpers/stellar"; -import { getAttrsFromSorobanHorizonOp, SorobanTokenInterface } from "./soroban"; +import { getAttrsFromSorobanHorizonOp } from "./soroban"; export const LP_IDENTIFIER = ":lp"; diff --git a/extension/src/popup/helpers/soroban.ts b/extension/src/popup/helpers/soroban.ts index e2da5705a0..c141b3bdc6 100644 --- a/extension/src/popup/helpers/soroban.ts +++ b/extension/src/popup/helpers/soroban.ts @@ -2,14 +2,8 @@ import BigNumber from "bignumber.js"; import * as SorobanClient from "soroban-client"; import { HorizonOperation, TokenBalances } from "@shared/api/types"; -import { decodeU32, valueToI128String } from "@shared/api/helpers/soroban"; import { NetworkDetails } from "@shared/constants/stellar"; -import { SorobanContextInterface } from "popup/SorobanContext"; - -export enum SorobanTokenInterface { - transfer = "transfer", - mint = "mint", -} +import { SorobanTokenInterface } from "@shared/constants/soroban/token"; export const SOROBAN_OPERATION_TYPES = [ "invoke_host_function", @@ -37,6 +31,22 @@ export const getAssetDecimals = ( return CLASSIC_ASSET_DECIMALS; }; +export const getTokenBalance = ( + tokenBalances: TokenBalances, + contractId: string, +) => { + const balance = tokenBalances.find(({ contractId: id }) => id === contractId); + + if (!balance) { + throw new Error("Balance not found"); + } + + return formatTokenAmount( + new BigNumber(balance.total), + Number(balance.decimals), + ); +}; + // Adopted from https://github.com/ethers-io/ethers.js/blob/master/packages/bignumber/src.ts/fixednumber.ts#L27 export const formatTokenAmount = (amount: BigNumber, decimals: number) => { let formatted = amount.toString(); @@ -90,44 +100,6 @@ export const parseTokenAmount = (value: string, decimals: number) => { return wholeValue.shiftedBy(decimals).plus(fractionValue); }; -export const getTokenBalance = ( - tokenBalances: TokenBalances, - contractId: string, -) => { - const balance = tokenBalances.find(({ contractId: id }) => id === contractId); - - if (!balance) { - throw new Error("Balance not found"); - } - - return formatTokenAmount( - new BigNumber(balance.total), - Number(balance.decimals), - ); -}; - -export const getContractDecimals = async ( - sorobanClient: SorobanContextInterface, - contractId: string, -) => { - const contract = new SorobanClient.Contract(contractId); - const server = sorobanClient.server; - - const tx = sorobanClient - .newTxBuilder() - .addOperation(contract.call("decimals")) - .setTimeout(SorobanClient.TimeoutInfinite) - .build(); - - const { results } = await server.simulateTransaction(tx); - - if (!results || results.length !== 1) { - throw new Error("Invalid response from simulateTransaction"); - } - const result = results[0]; - return decodeU32(result.xdr); -}; - export const getOpArgs = (fnName: string, args: SorobanClient.xdr.ScVal[]) => { let amount; let from; @@ -141,13 +113,13 @@ export const getOpArgs = (fnName: string, args: SorobanClient.xdr.ScVal[]) => { to = SorobanClient.StrKey.encodeEd25519PublicKey( args[1].address().accountId().ed25519(), ); - amount = valueToI128String(args[2]); + amount = SorobanClient.scValToNative(args[2]); break; case SorobanTokenInterface.mint: to = SorobanClient.StrKey.encodeEd25519PublicKey( args[0].address().accountId().ed25519(), ); - amount = args[1].i128().lo().low; + amount = SorobanClient.scValToNative(args[1]); break; default: amount = 0; @@ -175,10 +147,10 @@ const getRootInvocationArgs = ( } const contractId = SorobanClient.StrKey.encodeContract( - invokedContract[0].address().contractId(), + invokedContract.contractAddress().contractId(), ); - const fnName = invokedContract[1].sym().toString(); - const args = invokedContract.slice(2); + const fnName = invokedContract.functionName().toString(); + const args = invokedContract.args(); // TODO: figure out how to make this extensible to all contract functions if ( diff --git a/extension/src/popup/helpers/useSetupSigningFlow.ts b/extension/src/popup/helpers/useSetupSigningFlow.ts new file mode 100644 index 0000000000..ca2e9160fe --- /dev/null +++ b/extension/src/popup/helpers/useSetupSigningFlow.ts @@ -0,0 +1,138 @@ +import { useEffect, useRef, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { AppDispatch } from "popup/App"; +import { signTransaction, rejectTransaction } from "popup/ducks/access"; + +import { Account } from "@shared/api/types"; + +import { + allAccountsSelector, + confirmPassword, + hardwareWalletTypeSelector, + hasPrivateKeySelector, + makeAccountActive, + publicKeySelector, +} from "popup/ducks/accountServices"; + +import { + ShowOverlayStatus, + startHwSign, + transactionSubmissionSelector, +} from "popup/ducks/transactionSubmission"; + +export function useSetupSigningFlow( + reject: typeof rejectTransaction, + signFn: typeof signTransaction, + transactionXdr: string, + accountToSign?: string, +) { + const [isConfirming, setIsConfirming] = useState(false); + const [isPasswordRequired, setIsPasswordRequired] = useState(false); + const [startedHwSign, setStartedHwSign] = useState(false); + const [accountNotFound, setAccountNotFound] = useState(false); + const [currentAccount, setCurrentAccount] = useState({} as Account); + + const dispatch: AppDispatch = useDispatch(); + const allAccounts = useSelector(allAccountsSelector); + const hasPrivateKey = useSelector(hasPrivateKeySelector); + const hardwareWalletType = useSelector(hardwareWalletTypeSelector); + const publicKey = useSelector(publicKeySelector); + + // the public key the user had selected before starting this flow + const defaultPublicKey = useRef(publicKey); + const allAccountsMap = useRef({} as { [key: string]: Account }); + const isHardwareWallet = !!hardwareWalletType; + const { + hardwareWalletData: { status: hwStatus }, + } = useSelector(transactionSubmissionSelector); + + const rejectAndClose = () => { + dispatch(reject()); + window.close(); + }; + + const signAndClose = async () => { + if (isHardwareWallet) { + dispatch( + startHwSign({ transactionXDR: transactionXdr, shouldSubmit: false }), + ); + setStartedHwSign(true); + } else { + await dispatch(signFn()); + window.close(); + } + }; + + const handleApprove = async () => { + setIsConfirming(true); + + if (hasPrivateKey) { + await signAndClose(); + } else { + setIsPasswordRequired(true); + } + + setIsConfirming(false); + }; + + const verifyPasswordThenSign = async (password: string) => { + const confirmPasswordResp = await dispatch(confirmPassword(password)); + + if (confirmPassword.fulfilled.match(confirmPasswordResp)) { + await signAndClose(); + } + }; + + useEffect(() => { + if (startedHwSign && hwStatus === ShowOverlayStatus.IDLE) { + window.close(); + } + }, [startedHwSign, hwStatus]); + + useEffect(() => { + // handle auto selecting the right account based on `accountToSign` + let autoSelectedAccountDetails; + + allAccounts.forEach((account) => { + if (accountToSign) { + // does the user have the `accountToSign` somewhere in the accounts list? + if (account.publicKey === accountToSign) { + // if the `accountToSign` is found, but it isn't active, make it active + if (defaultPublicKey.current !== account.publicKey) { + dispatch(makeAccountActive(account.publicKey)); + } + + // save the details of the `accountToSign` + autoSelectedAccountDetails = account; + } + } + + // create an object so we don't need to keep iterating over allAccounts when we switch accounts + allAccountsMap.current[account.publicKey] = account; + }); + + if (!autoSelectedAccountDetails) { + setAccountNotFound(true); + } + }, [accountToSign, allAccounts, dispatch]); + + useEffect(() => { + // handle any changes to the current acct - whether by auto select or manual select + setCurrentAccount(allAccountsMap.current[publicKey] || ({} as Account)); + }, [allAccounts, publicKey]); + + return { + allAccounts, + accountNotFound, + currentAccount, + handleApprove, + isHardwareWallet, + publicKey, + hwStatus, + isConfirming, + isPasswordRequired, + rejectAndClose, + setIsPasswordRequired, + verifyPasswordThenSign, + }; +} diff --git a/extension/src/popup/locales/en/translation.json b/extension/src/popup/locales/en/translation.json index 4e1b95cb00..ad89ef8141 100644 --- a/extension/src/popup/locales/en/translation.json +++ b/extension/src/popup/locales/en/translation.json @@ -53,7 +53,7 @@ "Asset not found": "Asset not found", "Assets found in this domain": "Assets found in this domain", "At least one uppercase letter": "At least one uppercase letter", - "Auth": "Auth", + "Authorization Entry": "Authorization Entry", "Authorization Immutable": "Authorization Immutable", "Authorization Required": "Authorization Required", "Authorization Required; Authorization Immutable": "Authorization Required; Authorization Immutable", @@ -62,10 +62,10 @@ "Authorization Revocable": "Authorization Revocable", "Authorization Revocable; Authorization Immutable": "Authorization Revocable; Authorization Immutable", "available": "available", - "Avoid scams and keep your account safe": "Avoid scams and keep your account safe", "Awesome, you passed the test": { " Keep your recovery phrase safe, it’s your responsibility": "Awesome, you passed the test. Keep your recovery phrase safe, it’s your responsibility." }, + "Awesome, you passed the test! Pin the extension in your browser to access it easily": "Awesome, you passed the test! Pin the extension in your browser to access it easily.", "Balance Id": "Balance Id", "Base fee": "Base fee", "Before You Add This Asset": "Before You Add This Asset", @@ -130,6 +130,7 @@ "Enter passphrase": "Enter passphrase", "Enter password": "Enter password", "Enter Password": "Enter Password", + "Enter Soroban RPC URL": "Enter Soroban RPC URL", "Enter Token ID": "Enter Token ID", "Enter your 12 word phrase to restore your wallet": "Enter your 12 word phrase to restore your wallet", "Enter your account password to authorize this transaction": "Enter your account password to authorize this transaction.", @@ -168,7 +169,6 @@ }, "Friendbot URL": "Friendbot URL", "From": "From", - "Func": "Func", "Function Name": "Function Name", "Fund with Friendbot": "Fund with Friendbot", "Got it": "Got it", @@ -176,7 +176,9 @@ "High Threshold": "High Threshold", "History": "History", "Home": "Home", + "HORIZON RPC URL": "HORIZON RPC URL", "I have my 12 word seed phrase": "I have my 12 word seed phrase", + "I have my recovery phrase safe": "I have my recovery phrase safe", "I have read and agree to": "I have read and agree to", "I’m aware Freighter can’t recover the imported secret key": "I’m aware Freighter can’t recover the imported secret key", "I’m going to need a seed phrase": "I’m going to need a seed phrase", @@ -201,13 +203,17 @@ "Invalid Format Asset": "Invalid Format Asset", "invalid public key": "invalid public key", "INVALID STELLAR ADDRESS": "INVALID STELLAR ADDRESS", + "Invocation": "Invocation", "is requesting approval to a": "is requesting approval to a", "is requesting approval to sign a blob of data": "is requesting approval to sign a blob of data", + "is requesting approval to sign an authorization entry": "is requesting approval to sign an authorization entry", "Issuer": "Issuer", "Keep it in a safe place": "Keep it in a safe place.", + "Keep your account safe": "Keep your account safe", "Keep your recovery phrase in a safe and secure place": { " Anyone who has access to this phrase has access to your account and to the funds in it, so save it in a safe and secure place": "Keep your recovery phrase in a safe and secure place. Anyone who has access to this phrase has access to your account and to the funds in it, so save it in a safe and secure place." }, + "Keep your recovery phrase safe, it’s your responsibility": "Keep your recovery phrase safe, it’s your responsibility.", "Learn more": "Learn more", "Learn More": "Learn More", "Learn more about account creation": "Learn more about account creation", @@ -245,7 +251,7 @@ "Min Amount B": "Min Amount B", "Min Price": "Min Price", "Minimum Received": "Minimum Received", - "Mint": "Mint", + "Minted": "Minted", "Multiple assets": "Multiple assets", "Multiple assets have a similar code, please check the domain before adding": "Multiple assets have a similar code, please check the domain before adding.", "must be below": "must be below", @@ -278,7 +284,6 @@ "Parameters": "Parameters", "Passphrase": "Passphrase", "Paths": "Paths", - "Pin the extension in your browser to access it easily": "Pin the extension in your browser to access it easily.", "Please check if the network information is correct and try again": { " Alternatively, this network may not be operational": "Please check if the network information is correct and try again. Alternatively, this network may not be operational." }, @@ -337,7 +342,9 @@ "Share Public Key": "Share Public Key", "Show recovery phrase": "Show recovery phrase", "Signer": "Signer", + "Signing arbitrary data with a hardware wallet is currently not supported": "Signing arbitrary data with a hardware wallet is currently not supported.", "Signing this transaction is not possible at the moment": "Signing this transaction is not possible at the moment.", + "SOROBAN RPC URL": "SOROBAN RPC URL", "Source": "Source", "Sponsored Id": "Sponsored Id", "Stellar address is not funded": "Stellar address is not funded", @@ -346,7 +353,6 @@ "swap": "swap", "Swap": "Swap", "SWAP": "SWAP", - "Swap • {{date}}": "Swap • {{date}}", "Swap failed": "Swap failed", "swapped": "swapped", "Swapped": "Swapped", @@ -399,7 +405,7 @@ "Unable to connect to": "Unable to connect to", "Unable to search for assets": "Unable to search for assets", "Unsafe": "Unsafe", - "URL": "URL", + "Unsupported signing method": "Unsupported signing method", "Validate addresses that require a memo": "Validate addresses that require a memo", "Value Data": "Value Data", "Value Type": "Value Type", @@ -408,15 +414,14 @@ "View on": "View on", "View public key": "View public key", "Wallet Address": "Wallet Address", + "Wallet created successfully!": "Wallet created successfully!", "Want to add another account?": "Want to add another account?", "WEBSITE CONNECTION IS NOT SECURE": "WEBSITE CONNECTION IS NOT SECURE", "Weight": "Weight", "Welcome! Is this your first time using Freighter?": "Welcome! Is this your first time using Freighter?", "which is not available on Freighter": { - " If you own this account, you can import it into Freighter to complete this request": "which is not available on Freighter. If you own this account, you can import it into Freighter to complete this request.", - " If you own this account, you can import it into Freighter to complete this transaction": "which is not available on Freighter. If you own this account, you can import it into Freighter to complete this transaction." + " If you own this account, you can import it into Freighter to complete this request": "which is not available on Freighter. If you own this account, you can import it into Freighter to complete this request." }, - "Woo, you’re in!": "Woo, you’re in!", "You are attempting to sign arbitrary data": { " Please use extreme caution and understand the implications of signing this data": "You are attempting to sign arbitrary data. Please use extreme caution and understand the implications of signing this data." }, @@ -431,13 +436,11 @@ " The friendbot is a horizon API endpoint that will fund an account with 10,000 lumens": "You can fund this account using the friendbot tool. The friendbot is a horizon API endpoint that will fund an account with 10,000 lumens." }, "You must have a balance of": "You must have a balance of", - "You successfully imported your account": { - " Keep your recovery phrase safe, it’s your responsibility": "You successfully imported your account. Keep your recovery phrase safe, it’s your responsibility" - }, "You’re good to go!": "You’re good to go!", "Your account balance is not sufficient for this transaction": { " Please review the transaction and try again": "Your account balance is not sufficient for this transaction. Please review the transaction and try again." }, + "Your Freighter install is complete": "Your Freighter install is complete", "Your recovery phrase gives you access to your account and is the": "Your recovery phrase gives you access to your account and is the", "Your Stellar secret key": "Your Stellar secret key" } diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index f6cef92ca4..2e69ea60fd 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -53,7 +53,7 @@ "Asset not found": "Asset not found", "Assets found in this domain": "Assets found in this domain", "At least one uppercase letter": "At least one uppercase letter", - "Auth": "Auth", + "Authorization Entry": "Authorization Entry", "Authorization Immutable": "Authorization Immutable", "Authorization Required": "Authorization Required", "Authorization Required; Authorization Immutable": "Authorization Required; Authorization Immutable", @@ -62,10 +62,10 @@ "Authorization Revocable": "Authorization Revocable", "Authorization Revocable; Authorization Immutable": "Authorization Revocable; Authorization Immutable", "available": "available", - "Avoid scams and keep your account safe": "Avoid scams and keep your account safe", "Awesome, you passed the test": { " Keep your recovery phrase safe, it’s your responsibility": "Awesome, you passed the test. Keep your recovery phrase safe, it’s your responsibility." }, + "Awesome, you passed the test! Pin the extension in your browser to access it easily": "Awesome, you passed the test! Pin the extension in your browser to access it easily.", "Balance Id": "Balance Id", "Base fee": "Base fee", "Before You Add This Asset": "Before You Add This Asset", @@ -130,6 +130,7 @@ "Enter passphrase": "Enter passphrase", "Enter password": "Enter password", "Enter Password": "Enter Password", + "Enter Soroban RPC URL": "Enter Soroban RPC URL", "Enter Token ID": "Enter Token ID", "Enter your 12 word phrase to restore your wallet": "Enter your 12 word phrase to restore your wallet", "Enter your account password to authorize this transaction": "Enter your account password to authorize this transaction.", @@ -168,7 +169,6 @@ }, "Friendbot URL": "Friendbot URL", "From": "From", - "Func": "Func", "Function Name": "Function Name", "Fund with Friendbot": "Fund with Friendbot", "Got it": "Got it", @@ -176,7 +176,9 @@ "High Threshold": "High Threshold", "History": "History", "Home": "Home", + "HORIZON RPC URL": "HORIZON RPC URL", "I have my 12 word seed phrase": "I have my 12 word seed phrase", + "I have my recovery phrase safe": "I have my recovery phrase safe", "I have read and agree to": "I have read and agree to", "I’m aware Freighter can’t recover the imported secret key": "I’m aware Freighter can’t recover the imported secret key", "I’m going to need a seed phrase": "I’m going to need a seed phrase", @@ -201,13 +203,17 @@ "Invalid Format Asset": "Invalid Format Asset", "invalid public key": "invalid public key", "INVALID STELLAR ADDRESS": "INVALID STELLAR ADDRESS", + "Invocation": "Invocation", "is requesting approval to a": "is requesting approval to a", "is requesting approval to sign a blob of data": "is requesting approval to sign a blob of data", + "is requesting approval to sign an authorization entry": "is requesting approval to sign an authorization entry", "Issuer": "Issuer", "Keep it in a safe place": "Keep it in a safe place.", + "Keep your account safe": "Keep your account safe", "Keep your recovery phrase in a safe and secure place": { " Anyone who has access to this phrase has access to your account and to the funds in it, so save it in a safe and secure place": "Keep your recovery phrase in a safe and secure place. Anyone who has access to this phrase has access to your account and to the funds in it, so save it in a safe and secure place." }, + "Keep your recovery phrase safe, it’s your responsibility": "Keep your recovery phrase safe, it’s your responsibility.", "Learn more": "Learn more", "Learn More": "Learn More", "Learn more about account creation": "Learn more about account creation", @@ -245,7 +251,7 @@ "Min Amount B": "Min Amount B", "Min Price": "Min Price", "Minimum Received": "Minimum Received", - "Mint": "Mint", + "Minted": "Minted", "Multiple assets": "Multiple assets", "Multiple assets have a similar code, please check the domain before adding": "Multiple assets have a similar code, please check the domain before adding.", "must be below": "must be below", @@ -278,7 +284,6 @@ "Parameters": "Parameters", "Passphrase": "Passphrase", "Paths": "Paths", - "Pin the extension in your browser to access it easily": "Pin the extension in your browser to access it easily.", "Please check if the network information is correct and try again": { " Alternatively, this network may not be operational": "Please check if the network information is correct and try again. Alternatively, this network may not be operational." }, @@ -337,7 +342,9 @@ "Share Public Key": "Share Public Key", "Show recovery phrase": "Show recovery phrase", "Signer": "Signer", + "Signing arbitrary data with a hardware wallet is currently not supported": "Signing arbitrary data with a hardware wallet is currently not supported.", "Signing this transaction is not possible at the moment": "Signing this transaction is not possible at the moment.", + "SOROBAN RPC URL": "SOROBAN RPC URL", "Source": "Source", "Sponsored Id": "Sponsored Id", "Stellar address is not funded": "Stellar address is not funded", @@ -346,7 +353,6 @@ "swap": "swap", "Swap": "Swap", "SWAP": "SWAP", - "Swap • {{date}}": "Swap • {{date}}", "Swap failed": "Swap failed", "swapped": "swapped", "Swapped": "Swapped", @@ -399,7 +405,7 @@ "Unable to connect to": "Unable to connect to", "Unable to search for assets": "Unable to search for assets", "Unsafe": "Unsafe", - "URL": "URL", + "Unsupported signing method": "Unsupported signing method", "Validate addresses that require a memo": "Validate addresses that require a memo", "Value Data": "Value Data", "Value Type": "Value Type", @@ -408,15 +414,14 @@ "View on": "View on", "View public key": "View public key", "Wallet Address": "Wallet Address", + "Wallet created successfully!": "Wallet created successfully!", "Want to add another account?": "Want to add another account?", "WEBSITE CONNECTION IS NOT SECURE": "WEBSITE CONNECTION IS NOT SECURE", "Weight": "Weight", "Welcome! Is this your first time using Freighter?": "Welcome! Is this your first time using Freighter?", "which is not available on Freighter": { - " If you own this account, you can import it into Freighter to complete this request": "which is not available on Freighter. If you own this account, you can import it into Freighter to complete this request.", - " If you own this account, you can import it into Freighter to complete this transaction": "which is not available on Freighter. If you own this account, you can import it into Freighter to complete this transaction." + " If you own this account, you can import it into Freighter to complete this request": "which is not available on Freighter. If you own this account, you can import it into Freighter to complete this request." }, - "Woo, you’re in!": "Woo, you’re in!", "You are attempting to sign arbitrary data": { " Please use extreme caution and understand the implications of signing this data": "You are attempting to sign arbitrary data. Please use extreme caution and understand the implications of signing this data." }, @@ -431,13 +436,11 @@ " The friendbot is a horizon API endpoint that will fund an account with 10,000 lumens": "You can fund this account using the friendbot tool. The friendbot is a horizon API endpoint that will fund an account with 10,000 lumens." }, "You must have a balance of": "You must have a balance of", - "You successfully imported your account": { - " Keep your recovery phrase safe, it’s your responsibility": "You successfully imported your account. Keep your recovery phrase safe, it’s your responsibility" - }, "You’re good to go!": "You’re good to go!", "Your account balance is not sufficient for this transaction": { " Please review the transaction and try again": "Your account balance is not sufficient for this transaction. Please review the transaction and try again." }, + "Your Freighter install is complete": "Your Freighter install is complete", "Your recovery phrase gives you access to your account and is the": "Your recovery phrase gives you access to your account and is the", "Your Stellar secret key": "Your Stellar secret key" } diff --git a/extension/src/popup/metrics/access.ts b/extension/src/popup/metrics/access.ts index c3cb0fe918..a547e2d488 100644 --- a/extension/src/popup/metrics/access.ts +++ b/extension/src/popup/metrics/access.ts @@ -3,6 +3,7 @@ import { METRIC_NAMES } from "popup/constants/metricsNames"; import { grantAccess, rejectAccess, + signEntry, signTransaction, signBlob, rejectTransaction, @@ -36,3 +37,11 @@ registerHandler(signBlob.fulfilled, () => { accountType: metricsData.accountType, }); }); +registerHandler(signEntry.fulfilled, () => { + const metricsData: MetricsData = JSON.parse( + localStorage.getItem(METRICS_DATA) || "{}", + ); + emitMetric(METRIC_NAMES.signAuthEntry, { + accountType: metricsData.accountType, + }); +}); diff --git a/extension/src/popup/metrics/views.ts b/extension/src/popup/metrics/views.ts index e063fbf550..7f53341ad8 100644 --- a/extension/src/popup/metrics/views.ts +++ b/extension/src/popup/metrics/views.ts @@ -17,7 +17,9 @@ const routeToEventName = { [ROUTES.connectWallet]: METRIC_NAMES.viewConnectWallet, [ROUTES.connectWalletPlugin]: METRIC_NAMES.viewConnectWalletPlugin, [ROUTES.connectLedger]: METRIC_NAMES.viewConnectLedger, + [ROUTES.signBlob]: METRIC_NAMES.viewSignBlob, [ROUTES.signTransaction]: METRIC_NAMES.viewSignTransaction, + [ROUTES.signAuthEntry]: METRIC_NAMES.viewSignAuthEntry, [ROUTES.grantAccess]: METRIC_NAMES.viewGrantAccess, [ROUTES.mnemonicPhrase]: METRIC_NAMES.viewMnemonicPhrase, [ROUTES.mnemonicPhraseConfirm]: METRIC_NAMES.viewMnemonicPhraseConfirm, @@ -74,7 +76,7 @@ registerHandler(navigate, (_, a) => { throw new Error(`Didn't find a metric event name for path '${pathname}'`); } - // "/sign-transaction" and "/grant-access" require additionak metrics on loaded page + // "/sign-transaction" and "/grant-access" require additional metrics on loaded page if (pathname === ROUTES.grantAccess) { const { url } = parsedSearchParam(search); const METRIC_OPTION_DOMAIN = { @@ -87,17 +89,27 @@ registerHandler(navigate, (_, a) => { const { url } = parsedSearchParam(search); const info = getTransactionInfo(search); - if (!("blob" in info)) { - const { operations, operationTypes } = info; - const METRIC_OPTIONS = { - domain: getUrlDomain(url), - subdomain: getUrlHostname(url), - number_of_operations: operations.length, - operationTypes, - }; + const { operations, operationTypes } = info; + const METRIC_OPTIONS = { + domain: getUrlDomain(url), + subdomain: getUrlHostname(url), + number_of_operations: operations.length, + operationTypes, + }; + + emitMetric(eventName, METRIC_OPTIONS); + } else if ( + pathname === ROUTES.signAuthEntry || + pathname === ROUTES.signBlob + ) { + const { url } = parsedSearchParam(search); + + const METRIC_OPTIONS = { + domain: getUrlDomain(url), + subdomain: getUrlHostname(url), + }; - emitMetric(eventName, METRIC_OPTIONS); - } + emitMetric(eventName, METRIC_OPTIONS); } else { emitMetric(eventName); } diff --git a/extension/src/popup/views/Account/index.tsx b/extension/src/popup/views/Account/index.tsx index 09963b28f2..c3cf7e8b46 100644 --- a/extension/src/popup/views/Account/index.tsx +++ b/extension/src/popup/views/Account/index.tsx @@ -2,6 +2,7 @@ import React, { useContext, useState, useEffect, useRef } from "react"; import { useDispatch, useSelector } from "react-redux"; import { Button, CopyText, Icon, NavButton } from "@stellar/design-system"; import { useTranslation } from "react-i18next"; +import { Networks } from "soroban-client"; import { getAccountHistory } from "@shared/api/internal"; import { @@ -47,6 +48,7 @@ import { navigateTo } from "popup/helpers/navigate"; import { AccountAssets } from "popup/components/account/AccountAssets"; import { AccountHeader } from "popup/components/account/AccountHeader"; import { AssetDetail } from "popup/components/account/AssetDetail"; +import { Loading } from "popup/components/Loading"; import { NotFundedMessage } from "popup/components/account/NotFundedMessage"; import { BottomNav } from "popup/components/BottomNav"; import { SorobanContext } from "../../SorobanContext"; @@ -102,7 +104,12 @@ export const Account = () => { dispatch(getBlockedDomains()); if (isExperimentalModeEnabled) { - dispatch(getTokenBalances({ sorobanClient })); + dispatch( + getTokenBalances({ + sorobanClient, + network: networkDetails.network as Networks, + }), + ); } return () => { @@ -174,7 +181,9 @@ export const Account = () => { /> ) : ( <> - {isLoading ? null : ( + {isLoading ? ( + + ) : (
{ const stellarExpertUrl = getStellarExpertUrl(networkDetails); // differentiate between if data is still loading and if no account history results came back from Horizon + const shouldLoadToken = + isExperimentalModeEnabled || networkDetails.network === NETWORKS.FUTURENET; + const isTokenBalanceLoading = + (getTokenBalancesStatus === ActionStatus.IDLE || + getTokenBalancesStatus === ActionStatus.PENDING) && + shouldLoadToken; const isAccountHistoryLoading = isExperimentalModeEnabled - ? historySegments === null || - getTokenBalancesStatus === ActionStatus.IDLE || - getTokenBalancesStatus === ActionStatus.PENDING + ? historySegments === null || isTokenBalanceLoading : historySegments === null; useEffect(() => { const isSupportedSorobanAccountItem = (operation: HorizonOperation) => - // TODO: add mint and other common token interactions getIsSupportedSorobanOp(operation, networkDetails); - setIsLoading(true); const createSegments = ( operations: HorizonOperation[], showSorobanTxs = false, @@ -157,18 +159,29 @@ export const AccountHistory = () => { createSegments(res.operations, isExperimentalModeEnabled), ); - if (isExperimentalModeEnabled) { - dispatch(getTokenBalances({ sorobanClient })); + if (shouldLoadToken) { + dispatch( + getTokenBalances({ + sorobanClient, + network: networkDetails.network as Networks, + }), + ); } } catch (e) { console.error(e); } + }; + + const getData = async () => { + setIsLoading(true); + await fetchAccountHistory(); setIsLoading(false); }; - fetchAccountHistory(); + + getData(); return () => { - if (isExperimentalModeEnabled) { + if (shouldLoadToken) { dispatch(resetSorobanTokensStatus()); } }; @@ -177,6 +190,7 @@ export const AccountHistory = () => { networkDetails, isExperimentalModeEnabled, sorobanClient, + shouldLoadToken, dispatch, ]); @@ -184,7 +198,7 @@ export const AccountHistory = () => { ) : (
- {isLoading ? ( + {isLoading || isTokenBalanceLoading ? (
diff --git a/extension/src/popup/views/FullscreenSuccessMessage/index.tsx b/extension/src/popup/views/FullscreenSuccessMessage/index.tsx index c66ddf0d39..94971e0036 100644 --- a/extension/src/popup/views/FullscreenSuccessMessage/index.tsx +++ b/extension/src/popup/views/FullscreenSuccessMessage/index.tsx @@ -1,7 +1,7 @@ import React from "react"; import { useLocation } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { Button, Icon, Notification } from "@stellar/design-system"; +import { Button, Notification } from "@stellar/design-system"; import { emitMetric } from "helpers/metrics"; @@ -12,7 +12,6 @@ import { SubmitButtonWrapper } from "popup/basics/Forms"; import { FullscreenStyle } from "popup/components/FullscreenStyle"; import { Header } from "popup/components/Header"; import { OnboardingHeader } from "popup/components/Onboarding"; -import SuccessIllo from "popup/assets/illo-success-screen.svg"; import ExtensionIllo from "popup/assets/illo-extension.png"; import "./styles.scss"; @@ -27,16 +26,12 @@ const AvoidScamsWarningBlock = () => { return (
- } - title={t("Avoid scams and keep your account safe")} - > +
  • {t( - "Freighter will never ask for your recovery phrase unless you’re actively importing your account using the browser extension - never on an external website.", + "Freighter will never ask for your recovery phrase unless you’re actively importing your account using the browser extension - never on an external website", )}
  • @@ -72,8 +67,8 @@ const MnemonicPhraseConfirmedMessage = () => { @@ -153,15 +151,8 @@ export const FullscreenSuccessMessage = () => {
    -
    - Success -
    - {t("Woo, you’re in!")} + {t("Wallet created successfully!")}
    {IS_MNEMONIC_PHRASE_STATE ? ( diff --git a/extension/src/popup/views/FullscreenSuccessMessage/styles.scss b/extension/src/popup/views/FullscreenSuccessMessage/styles.scss index cfa1d3490d..908f43d4b5 100644 --- a/extension/src/popup/views/FullscreenSuccessMessage/styles.scss +++ b/extension/src/popup/views/FullscreenSuccessMessage/styles.scss @@ -15,6 +15,7 @@ ul { list-style-type: disc; } + li { margin-top: 1rem; line-height: 1.5rem; @@ -45,7 +46,7 @@ justify-content: start; padding: 0 0 2rem 0; color: var(--color-gray-90); - max-width: 24rem; + max-width: 31rem; } &__infoBlock { @@ -53,11 +54,14 @@ &__list { list-style: inside; - margin-top: -0.25rem; ::before { content: none !important; } + + li:not(:last-of-type) { + margin-bottom: 1rem; + } } } @@ -75,14 +79,17 @@ } &__copy { - text-align: center; font-size: 1rem; line-height: 1.5rem; font-weight: var(--font-weight-regular); margin-bottom: 1.5rem; p { - color: rgba(255, 255, 255, 0.6); + color: var(--color-gray-70) !important; + + strong { + color: var(--color-gray-80) !important; + } } } } diff --git a/extension/src/popup/views/PinExtension/index.tsx b/extension/src/popup/views/PinExtension/index.tsx index 6001b501b1..8cbd9e522c 100644 --- a/extension/src/popup/views/PinExtension/index.tsx +++ b/extension/src/popup/views/PinExtension/index.tsx @@ -1,10 +1,12 @@ import React from "react"; import { useTranslation } from "react-i18next"; +import { Button, Icon } from "@stellar/design-system"; -import ExtensionsMenu from "popup/assets/extensions-menu.png"; -import ExtensionsPin from "popup/assets/extensions-pin.png"; +import ExtensionsPin from "popup/assets/illo-pin-extension.svg"; +import { SubmitButtonWrapper } from "popup/basics/Forms"; import { Header } from "popup/components/Header"; import { FullscreenStyle } from "popup/components/FullscreenStyle"; +import { OnboardingHeader } from "popup/components/Onboarding"; import "./styles.scss"; @@ -17,24 +19,38 @@ export const PinExtension = () => {
    -
    - {t("Pin the extension in your browser to access it easily.")} -
    -
    - 1.{" "} - {t( - "Click on the extensions button at the top of your browser’s bar", - )} -
    -
    - Extensions Menu -
    + + {t("Your Freighter install is complete")} +
    - 2. {t("Click on Freighter’s pin button to have it always visible")} +
    + 1.{" "} + {t( + "Click on the extensions button at the top of your browser’s bar", + )} +
    +
    + 2.{" "} + {t("Click on Freighter’s pin button to have it always visible")} +
    Extensions Pin
    + + +
    diff --git a/extension/src/popup/views/PinExtension/styles.scss b/extension/src/popup/views/PinExtension/styles.scss index ea88a9dd1f..b56755bc71 100644 --- a/extension/src/popup/views/PinExtension/styles.scss +++ b/extension/src/popup/views/PinExtension/styles.scss @@ -6,7 +6,7 @@ align-items: center; &__wrapper { - max-width: 24rem; + max-width: 31rem; } &__title { @@ -14,16 +14,23 @@ line-height: 2.25rem; text-align: center; color: var(--color-gray-90); + margin-bottom: 1rem; font-weight: var(--font-weight-medium); } &__caption { line-height: 1.5rem; - margin: 2rem 0; - color: var(--color-gray-60); + margin-bottom: 2rem; + color: var(--color-gray-70); + + div:not(:last-child) { + margin-bottom: 0.4rem; + } } &__img { + margin-bottom: 2rem; + img { width: 100%; } diff --git a/extension/src/popup/views/SignAuthEntry/index.tsx b/extension/src/popup/views/SignAuthEntry/index.tsx new file mode 100644 index 0000000000..8f1c9698c2 --- /dev/null +++ b/extension/src/popup/views/SignAuthEntry/index.tsx @@ -0,0 +1,223 @@ +import React, { useState } from "react"; +import { Button, Card, Icon, Notification } from "@stellar/design-system"; +import { useLocation } from "react-router-dom"; +import { useSelector } from "react-redux"; +import { useTranslation, Trans } from "react-i18next"; +import { + ButtonsContainer, + ModalHeader, + ModalWrapper, +} from "popup/basics/Modal"; + +import { truncatedPublicKey } from "helpers/stellar"; +import { LedgerSign } from "popup/components/hardwareConnect/LedgerSign"; +import { AccountListIdenticon } from "popup/components/identicons/AccountListIdenticon"; +import { AccountList, OptionTag } from "popup/components/account/AccountList"; +import { PunycodedDomain } from "popup/components/PunycodedDomain"; +import { SlideupModal } from "popup/components/SlideupModal"; +import { + FirstTimeWarningMessage, + WarningMessageVariant, + WarningMessage, +} from "popup/components/WarningMessages"; +import { signEntry, rejectAuthEntry } from "popup/ducks/access"; +import { settingsExperimentalModeSelector } from "popup/ducks/settings"; +import { ShowOverlayStatus } from "popup/ducks/transactionSubmission"; +import { VerifyAccount } from "popup/views/VerifyAccount"; + +import { EntryToSign, parsedSearchParam } from "helpers/urls"; +import { AuthEntry } from "popup/components/signAuthEntry/AuthEntry"; + +import "./styles.scss"; +import { useSetupSigningFlow } from "popup/helpers/useSetupSigningFlow"; + +export const SignAuthEntry = () => { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + const location = useLocation(); + const { t } = useTranslation(); + const isExperimentalModeEnabled = useSelector( + settingsExperimentalModeSelector, + ); + + const params = parsedSearchParam(location.search) as EntryToSign; + const { accountToSign } = params; + + const { + allAccounts, + accountNotFound, + currentAccount, + isConfirming, + isPasswordRequired, + publicKey, + handleApprove, + hwStatus, + isHardwareWallet, + rejectAndClose, + setIsPasswordRequired, + verifyPasswordThenSign, + } = useSetupSigningFlow( + rejectAuthEntry, + signEntry, + params.entry, + accountToSign, + ); + + if (isHardwareWallet) { + return ( + + window.close()} + isActive + header={t("Unsupported signing method")} + > +

    + {t( + "Signing arbitrary data with a hardware wallet is currently not supported.", + )} +

    +
    +
    + ); + } + + if (!params.url.startsWith("https") && !isExperimentalModeEnabled) { + return ( + + window.close()} + isActive + variant={WarningMessageVariant.warning} + header={t("WEBSITE CONNECTION IS NOT SECURE")} + > +

    + + The website {{ domain: params.url }} does not use + an SSL certificate. For additional safety Freighter only works + with websites that provide an SSL certificate. + +

    +
    +
    + ); + } + + return isPasswordRequired ? ( + setIsPasswordRequired(false)} + customSubmit={verifyPasswordThenSign} + /> + ) : ( + <> + {hwStatus === ShowOverlayStatus.IN_PROGRESS && } +
    + + + {t("Confirm Data")} + + {isExperimentalModeEnabled ? ( + +

    + {t( + "You are interacting with data that may be using untested and changing schemas. Proceed at your own risk.", + )} +

    +
    + ) : null} + {!params.isDomainListedAllowed ? : null} +
    + + +
    + {t("is requesting approval to sign an authorization entry")} +
    +
    +
    + {t("Approve using")}: +
    +
    setIsDropdownOpen(true)} + > + + + +
    + +
    +
    +
    +
    + {accountNotFound && accountToSign ? ( +
    + } + title={t("Account not available")} + > + {t("The application is requesting a specific account")} ( + {truncatedPublicKey(accountToSign)}),{" "} + {t( + "which is not available on Freighter. If you own this account, you can import it into Freighter to complete this request.", + )} + +
    + ) : null} +
    + {/* Can replace AuthEntry once SignTx supports xdr classes */} + {/* */} + +
    + + + + + +
    + +
    +
    +
    + + ); +}; diff --git a/extension/src/popup/views/SignAuthEntry/styles.scss b/extension/src/popup/views/SignAuthEntry/styles.scss new file mode 100644 index 0000000000..ab1f214d34 --- /dev/null +++ b/extension/src/popup/views/SignAuthEntry/styles.scss @@ -0,0 +1,68 @@ +.SignAuthEntry { + height: var(--popup--height); + overflow: hidden; + position: relative; + + .AuthEntry { + display: -webkit-box; + width: 100%; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + word-wrap: break-word; + } + + &__inner-transaction { + border: 1px solid var(--pal-border-primary); + border-radius: 0.5rem; + height: 10rem; + opacity: 0.7; + overflow: auto; + padding: 1rem 2rem; + zoom: 0.7; + } + + &__info { + margin-bottom: 2rem; + } + + &__subject { + border-bottom: 1px solid var(--pal-border-secondary); + color: var(--pal-text-primary); + font-size: 0.875rem; + line-height: 1.5rem; + margin-bottom: 1rem; + padding-bottom: 1rem; + } + + &__approval { + margin-bottom: -1rem; + + &__title { + color: rgba(255, 255, 255, 0.6); + font-size: 0.875rem; + line-height: 1.5rem; + } + } + + &__current-account { + align-items: flex-start; + display: flex; + justify-content: space-between; + margin: 1rem 0 0.5rem 0; + width: 100%; + + &__chevron { + display: flex; + width: 0.75rem; + } + } + + &__modal { + padding: 0.5rem 0 0.5rem 1.5rem; + } + + &__account-not-found { + margin-top: 1rem; + } +} diff --git a/extension/src/popup/views/SignTransaction/SignBlob/index.tsx b/extension/src/popup/views/SignBlob/index.tsx similarity index 74% rename from extension/src/popup/views/SignTransaction/SignBlob/index.tsx rename to extension/src/popup/views/SignBlob/index.tsx index c0f263cf66..fc1b4dd94a 100644 --- a/extension/src/popup/views/SignTransaction/SignBlob/index.tsx +++ b/extension/src/popup/views/SignBlob/index.tsx @@ -1,104 +1,83 @@ -import React from "react"; +import React, { useState } from "react"; +import { useLocation } from "react-router-dom"; +import { useSelector } from "react-redux"; import { Button, Card, Icon, Notification } from "@stellar/design-system"; import { useTranslation, Trans } from "react-i18next"; - -import { truncatedPublicKey } from "helpers/stellar"; -import { signBlob } from "popup/ducks/access"; -import { confirmPassword } from "popup/ducks/accountServices"; - -import { - ButtonsContainer, - ModalHeader, - ModalWrapper, -} from "popup/basics/Modal"; - +import { signBlob, rejectBlob } from "popup/ducks/access"; import { AccountListIdenticon } from "popup/components/identicons/AccountListIdenticon"; import { AccountList, OptionTag } from "popup/components/account/AccountList"; import { PunycodedDomain } from "popup/components/PunycodedDomain"; +import { Blob } from "popup/components/signBlob"; +import { settingsExperimentalModeSelector } from "popup/ducks/settings"; import { WarningMessageVariant, WarningMessage, FirstTimeWarningMessage, } from "popup/components/WarningMessages"; + +import { ShowOverlayStatus } from "popup/ducks/transactionSubmission"; + +import { + ButtonsContainer, + ModalHeader, + ModalWrapper, +} from "popup/basics/Modal"; + import { LedgerSign } from "popup/components/hardwareConnect/LedgerSign"; import { SlideupModal } from "popup/components/SlideupModal"; import { VerifyAccount } from "popup/views/VerifyAccount"; -import { AppDispatch } from "popup/App"; -import { - ShowOverlayStatus, - startHwSign, -} from "popup/ducks/transactionSubmission"; - -import { Account } from "@shared/api/types"; -import { BlobToSign } from "helpers/urls"; +import { BlobToSign, parsedSearchParam } from "helpers/urls"; +import { truncatedPublicKey } from "helpers/stellar"; -import "../styles.scss"; -import { useDispatch } from "react-redux"; -import { Blob } from "popup/components/signBlob"; +import "./styles.scss"; +import { useSetupSigningFlow } from "popup/helpers/useSetupSigningFlow"; -interface SignBlobBodyProps { - accountNotFound: boolean; - allAccounts: Account[]; - blob: BlobToSign; - currentAccount: Account; - handleApprove: (signAndClose: () => Promise) => () => Promise; - hwStatus: ShowOverlayStatus; - isConfirming: boolean; - isDropdownOpen: boolean; - isExperimentalModeEnabled: boolean; - isHardwareWallet: boolean; - isPasswordRequired: boolean; - publicKey: string; - rejectAndClose: () => void; - setIsDropdownOpen: (isRequired: boolean) => void; - setIsPasswordRequired: (isRequired: boolean) => void; - setStartedHwSign: (hasStarted: boolean) => void; -} +export const SignBlob = () => { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); -export const SignBlobBody = ({ - publicKey, - allAccounts, - isDropdownOpen, - handleApprove, - isConfirming, - accountNotFound, - rejectAndClose, - currentAccount, - setIsDropdownOpen, - setIsPasswordRequired, - isPasswordRequired, - blob, - isExperimentalModeEnabled, - hwStatus, - isHardwareWallet, - setStartedHwSign, -}: SignBlobBodyProps) => { - const dispatch: AppDispatch = useDispatch(); + const location = useLocation(); const { t } = useTranslation(); - const { accountToSign, domain, isDomainListedAllowed } = blob; - - const signAndClose = async () => { - if (isHardwareWallet) { - await dispatch( - startHwSign({ transactionXDR: blob.blob, shouldSubmit: false }), - ); - setStartedHwSign(true); - } else { - await dispatch(signBlob()); - window.close(); - } - }; + const isExperimentalModeEnabled = useSelector( + settingsExperimentalModeSelector, + ); - const _handleApprove = handleApprove(signAndClose); + const blob = parsedSearchParam(location.search) as BlobToSign; + const { accountToSign, domain, isDomainListedAllowed } = blob; - const verifyPasswordThenSign = async (password: string) => { - const confirmPasswordResp = await dispatch(confirmPassword(password)); + const { + allAccounts, + accountNotFound, + currentAccount, + isConfirming, + isPasswordRequired, + publicKey, + handleApprove, + hwStatus, + isHardwareWallet, + rejectAndClose, + setIsPasswordRequired, + verifyPasswordThenSign, + } = useSetupSigningFlow(rejectBlob, signBlob, blob.blob, accountToSign); - if (confirmPassword.fulfilled.match(confirmPasswordResp)) { - await signAndClose(); - } - }; + if (isHardwareWallet) { + return ( + + window.close()} + isActive + header={t("Unsupported signing method")} + > +

    + {t( + "Signing arbitrary data with a hardware wallet is currently not supported.", + )} +

    +
    +
    + ); + } if (!domain.startsWith("https") && !isExperimentalModeEnabled) { return ( @@ -222,7 +201,7 @@ export const SignBlobBody = ({ isFullWidth variant="primary" isLoading={isConfirming} - onClick={() => _handleApprove()} + onClick={() => handleApprove()} > {t("Approve")} diff --git a/extension/src/popup/views/SignBlob/styles.scss b/extension/src/popup/views/SignBlob/styles.scss new file mode 100644 index 0000000000..35b8153673 --- /dev/null +++ b/extension/src/popup/views/SignBlob/styles.scss @@ -0,0 +1,68 @@ +.SignBlob { + height: var(--popup--height); + overflow: hidden; + position: relative; + + .Blob { + display: -webkit-box; + width: 100%; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + word-wrap: break-word; + } + + &__inner-transaction { + border: 1px solid var(--pal-border-primary); + border-radius: 0.5rem; + height: 10rem; + opacity: 0.7; + overflow: auto; + padding: 1rem 2rem; + zoom: 0.7; + } + + &__info { + margin-bottom: 2rem; + } + + &__subject { + border-bottom: 1px solid var(--pal-border-secondary); + color: var(--pal-text-primary); + font-size: 0.875rem; + line-height: 1.5rem; + margin-bottom: 1rem; + padding-bottom: 1rem; + } + + &__approval { + margin-bottom: -1rem; + + &__title { + color: rgba(255, 255, 255, 0.6); + font-size: 0.875rem; + line-height: 1.5rem; + } + } + + &__current-account { + align-items: flex-start; + display: flex; + justify-content: space-between; + margin: 1rem 0 0.5rem 0; + width: 100%; + + &__chevron { + display: flex; + width: 0.75rem; + } + } + + &__modal { + padding: 0.5rem 0 0.5rem 1.5rem; + } + + &__account-not-found { + margin-top: 1rem; + } +} diff --git a/extension/src/popup/views/SignTransaction/SignTx/index.tsx b/extension/src/popup/views/SignTransaction/SignTx/index.tsx deleted file mode 100644 index 9b624707e3..0000000000 --- a/extension/src/popup/views/SignTransaction/SignTx/index.tsx +++ /dev/null @@ -1,417 +0,0 @@ -import React, { useCallback, useEffect } from "react"; -import { Button, Card, Icon, Notification } from "@stellar/design-system"; -import StellarSdk, { FederationServer, MuxedAccount } from "stellar-sdk"; -import * as SorobanSdk from "soroban-client"; -import { useTranslation, Trans } from "react-i18next"; - -import { TRANSACTION_WARNING } from "constants/transaction"; - -import { emitMetric } from "helpers/metrics"; -import { - isFederationAddress, - isMuxedAccount, - truncatedPublicKey, -} from "helpers/stellar"; -import { decodeMemo } from "popup/helpers/parseTransaction"; -import { TransactionHeading } from "popup/basics/TransactionHeading"; -import { signTransaction } from "popup/ducks/access"; - -import { - ButtonsContainer, - ModalHeader, - ModalWrapper, -} from "popup/basics/Modal"; - -import { METRIC_NAMES } from "popup/constants/metricsNames"; - -import { AccountListIdenticon } from "popup/components/identicons/AccountListIdenticon"; -import { AccountList, OptionTag } from "popup/components/account/AccountList"; -import { PunycodedDomain } from "popup/components/PunycodedDomain"; -import { - WarningMessageVariant, - WarningMessage, - FirstTimeWarningMessage, - FlaggedWarningMessage, -} from "popup/components/WarningMessages"; -import { Transaction } from "popup/components/signTransaction/Transaction"; -import { LedgerSign } from "popup/components/hardwareConnect/LedgerSign"; -import { SlideupModal } from "popup/components/SlideupModal"; - -import { VerifyAccount } from "popup/views/VerifyAccount"; -import { - ShowOverlayStatus, - startHwSign, -} from "popup/ducks/transactionSubmission"; - -import { Account } from "@shared/api/types"; -import { FlaggedKeys } from "types/transactions"; -import { AppDispatch } from "popup/App"; - -import "../styles.scss"; -import { TransactionInfo } from "popup/components/signTransaction/TransactionInfo"; -import { confirmPassword } from "popup/ducks/accountServices"; -import { useDispatch } from "react-redux"; - -interface SignTxBodyProps { - allAccountsMap: React.MutableRefObject<{ [key: string]: Account }>; - accountNotFound: boolean; - allAccounts: Account[]; - currentAccount: Account; - handleApprove: (signAndClose: () => Promise) => () => Promise; - hwStatus: ShowOverlayStatus; - isConfirming: boolean; - isDropdownOpen: boolean; - isExperimentalModeEnabled: boolean; - isHardwareWallet: boolean; - isPasswordRequired: boolean; - networkName: string; - networkPassphrase: string; - publicKey: string; - rejectAndClose: () => void; - setCurrentAccount: (account: Account) => void; - setIsDropdownOpen: (isRequired: boolean) => void; - setIsPasswordRequired: (isRequired: boolean) => void; - setStartedHwSign: (hasStarted: boolean) => void; - startedHwSign: boolean; - tx: { - accountToSign: string | undefined; - transactionXdr: string; - domain: string; - domainTitle: any; - isHttpsDomain: boolean; - operations: any; - operationTypes: any; - isDomainListedAllowed: boolean; - flaggedKeys: FlaggedKeys; - }; -} - -export const SignTxBody = ({ - allAccountsMap, - setCurrentAccount, - startedHwSign, - setStartedHwSign, - publicKey, - allAccounts, - isDropdownOpen, - handleApprove, - isConfirming, - accountNotFound, - rejectAndClose, - currentAccount, - setIsDropdownOpen, - setIsPasswordRequired, - isPasswordRequired, - tx, - isExperimentalModeEnabled, - networkName, - networkPassphrase, - hwStatus, - isHardwareWallet, -}: SignTxBodyProps) => { - const dispatch: AppDispatch = useDispatch(); - const { t } = useTranslation(); - - const { - accountToSign: _accountToSign, - transactionXdr, - domain, - isDomainListedAllowed, - isHttpsDomain, - flaggedKeys, - } = tx; - - /* - Reconstruct the tx from xdr as passing a tx through extension contexts - loses custom prototypes associated with some values. This is fine for most cases - where we just need a high level overview of the tx, like just a list of operations. - But in this case, we will need the hostFn prototype associated with Soroban tx operations. - */ - - const SDK = isExperimentalModeEnabled ? SorobanSdk : StellarSdk; - const transaction = SDK.TransactionBuilder.fromXDR( - transactionXdr, - networkPassphrase, - ); - - const { - _fee, - _innerTransaction, - _memo, - _networkPassphrase, - _sequence, - } = transaction; - - const isFeeBump = !!_innerTransaction; - const memo = decodeMemo(_memo); - let accountToSign = _accountToSign; - - useEffect(() => { - if (startedHwSign && hwStatus === ShowOverlayStatus.IDLE) { - window.close(); - } - }, [startedHwSign, hwStatus]); - - const signAndClose = async () => { - if (isHardwareWallet) { - await dispatch( - startHwSign({ transactionXDR: transactionXdr, shouldSubmit: false }), - ); - setStartedHwSign(true); - } else { - await dispatch(signTransaction()); - window.close(); - } - }; - - const _handleApprove = handleApprove(signAndClose); - - const verifyPasswordThenSign = async (password: string) => { - const confirmPasswordResp = await dispatch(confirmPassword(password)); - - if (confirmPassword.fulfilled.match(confirmPasswordResp)) { - await signAndClose(); - } - }; - - const flaggedKeyValues = Object.values(flaggedKeys); - const isUnsafe = flaggedKeyValues.some(({ tags }) => - tags.includes(TRANSACTION_WARNING.unsafe), - ); - const isMalicious = flaggedKeyValues.some(({ tags }) => - tags.includes(TRANSACTION_WARNING.malicious), - ); - const isMemoRequired = flaggedKeyValues.some( - ({ tags }) => tags.includes(TRANSACTION_WARNING.memoRequired) && !memo, - ); - - const resolveFederatedAddress = useCallback(async (inputDest) => { - let resolvedPublicKey; - try { - const fedResp = await FederationServer.resolve(inputDest); - resolvedPublicKey = fedResp.account_id; - } catch (e) { - console.error(e); - } - - return resolvedPublicKey; - }, []); - - const decodeAccountToSign = async () => { - if (_accountToSign) { - if (isMuxedAccount(_accountToSign)) { - const mAccount = MuxedAccount.fromAddress(_accountToSign, "0"); - accountToSign = mAccount.baseAccount().accountId(); - } - if (isFederationAddress(_accountToSign)) { - accountToSign = (await resolveFederatedAddress( - accountToSign, - )) as string; - } - } - }; - decodeAccountToSign(); - - useEffect(() => { - if (isMemoRequired) { - emitMetric(METRIC_NAMES.signTransactionMemoRequired); - } - if (isUnsafe) { - emitMetric(METRIC_NAMES.signTransactionUnsafe); - } - if (isMalicious) { - emitMetric(METRIC_NAMES.signTransactionMalicious); - } - }, [isMemoRequired, isMalicious, isUnsafe]); - - useEffect(() => { - // handle any changes to the current acct - whether by auto select or manual select - setCurrentAccount(allAccountsMap.current[publicKey] || ({} as Account)); - }, [allAccountsMap, allAccounts, publicKey, setCurrentAccount]); - - const isSubmitDisabled = isMemoRequired || isMalicious; - - if (_networkPassphrase !== networkPassphrase) { - return ( - - window.close()} - isActive - header={`${t("Freighter is set to")} ${networkName}`} - > -

    - {t("The transaction you’re trying to sign is on")}{" "} - {_networkPassphrase}. -

    -

    {t("Signing this transaction is not possible at the moment.")}

    -
    -
    - ); - } - - if (!isHttpsDomain && !isExperimentalModeEnabled) { - return ( - - window.close()} - isActive - variant={WarningMessageVariant.warning} - header={t("WEBSITE CONNECTION IS NOT SECURE")} - > -

    - - The website {{ domain }} does not use an SSL - certificate. For additional safety Freighter only works with - websites that provide an SSL certificate. - -

    -
    -
    - ); - } - return isPasswordRequired ? ( - setIsPasswordRequired(false)} - customSubmit={verifyPasswordThenSign} - /> - ) : ( - <> - {hwStatus === ShowOverlayStatus.IN_PROGRESS && } -
    - - - {t("Confirm Transaction")} - - {isExperimentalModeEnabled ? ( - -

    - {t( - "You are interacting with a transaction that may be using untested and changing schemas. Proceed at your own risk.", - )} -

    -
    - ) : null} - {flaggedKeyValues.length ? ( - - ) : null} - {!isDomainListedAllowed && !isSubmitDisabled ? ( - - ) : null} -
    - - -
    - {t("is requesting approval to a")}{" "} - {isFeeBump ? "fee bump " : ""} - {t("transaction")}: -
    -
    -
    - {t("Approve using")}: -
    -
    setIsDropdownOpen(true)} - > - - - -
    - -
    -
    -
    -
    - {accountNotFound && accountToSign ? ( -
    - } - title={t("Account not available")} - > - {t("The application is requesting a specific account")} ( - {truncatedPublicKey(accountToSign)}),{" "} - {t( - "which is not available on Freighter. If you own this account, you can import it into Freighter to complete this transaction.", - )} - -
    - ) : null} -
    - {isFeeBump ? ( -
    - -
    - ) : ( - - )} - {t("Transaction Info")} - -
    - - - - - -
    - -
    -
    -
    - - ); -}; diff --git a/extension/src/popup/views/SignTransaction/index.tsx b/extension/src/popup/views/SignTransaction/index.tsx index a34bfd6e82..85f6bd203c 100644 --- a/extension/src/popup/views/SignTransaction/index.tsx +++ b/extension/src/popup/views/SignTransaction/index.tsx @@ -1,169 +1,362 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { useLocation } from "react-router-dom"; -import { useDispatch, useSelector } from "react-redux"; -import { getTransactionInfo } from "helpers/stellar"; -import { rejectTransaction, rejectBlob } from "popup/ducks/access"; -import { - allAccountsSelector, - hasPrivateKeySelector, - makeAccountActive, - publicKeySelector, - hardwareWalletTypeSelector, -} from "popup/ducks/accountServices"; +import { useSelector } from "react-redux"; +import { useTranslation, Trans } from "react-i18next"; +import { Button, Card, Icon, Notification } from "@stellar/design-system"; +import * as SorobanSdk from "soroban-client"; +import StellarSdk, { FederationServer, MuxedAccount } from "stellar-sdk"; + +import { signTransaction, rejectTransaction } from "popup/ducks/access"; import { settingsNetworkDetailsSelector, settingsExperimentalModeSelector, } from "popup/ducks/settings"; +import { ShowOverlayStatus } from "popup/ducks/transactionSubmission"; + +import { TRANSACTION_WARNING } from "constants/transaction"; + +import { emitMetric } from "helpers/metrics"; +import { + getTransactionInfo, + isFederationAddress, + isMuxedAccount, + truncatedPublicKey, +} from "helpers/stellar"; +import { decodeMemo } from "popup/helpers/parseTransaction"; +import { useSetupSigningFlow } from "popup/helpers/useSetupSigningFlow"; +import { TransactionHeading } from "popup/basics/TransactionHeading"; + import { - ShowOverlayStatus, - transactionSubmissionSelector, -} from "popup/ducks/transactionSubmission"; + ButtonsContainer, + ModalHeader, + ModalWrapper, +} from "popup/basics/Modal"; -import { Account } from "@shared/api/types"; -import { AppDispatch } from "popup/App"; +import { METRIC_NAMES } from "popup/constants/metricsNames"; + +import { AccountListIdenticon } from "popup/components/identicons/AccountListIdenticon"; +import { AccountList, OptionTag } from "popup/components/account/AccountList"; +import { PunycodedDomain } from "popup/components/PunycodedDomain"; +import { + WarningMessageVariant, + WarningMessage, + FirstTimeWarningMessage, + FlaggedWarningMessage, +} from "popup/components/WarningMessages"; +import { Transaction } from "popup/components/signTransaction/Transaction"; +import { LedgerSign } from "popup/components/hardwareConnect/LedgerSign"; +import { SlideupModal } from "popup/components/SlideupModal"; + +import { VerifyAccount } from "popup/views/VerifyAccount"; import "./styles.scss"; -import { SignBlobBody } from "./SignBlob"; -import { SignTxBody } from "./SignTx"; + +import { FlaggedKeys } from "types/transactions"; +import { TransactionInfo } from "popup/components/signTransaction/TransactionInfo"; export const SignTransaction = () => { const location = useLocation(); - const blobOrTx = getTransactionInfo(location.search); - const isBlob = "blob" in blobOrTx; + const { t } = useTranslation(); + + const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const dispatch: AppDispatch = useDispatch(); - const { networkName, networkPassphrase } = useSelector( - settingsNetworkDetailsSelector, - ); const isExperimentalModeEnabled = useSelector( settingsExperimentalModeSelector, ); + const { networkName, networkPassphrase } = useSelector( + settingsNetworkDetailsSelector, + ); + + const tx = getTransactionInfo(location.search); - const hardwareWalletType = useSelector(hardwareWalletTypeSelector); - const isHardwareWallet = !!hardwareWalletType; const { - hardwareWalletData: { status: hwStatus }, - } = useSelector(transactionSubmissionSelector); + accountToSign: _accountToSign, + transactionXdr, + domain, + isDomainListedAllowed, + isHttpsDomain, + flaggedKeys, + } = tx; - const [startedHwSign, setStartedHwSign] = useState(false); - const [currentAccount, setCurrentAccount] = useState({} as Account); - const [isPasswordRequired, setIsPasswordRequired] = useState(false); - const [isConfirming, setIsConfirming] = useState(false); - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [accountNotFound, setAccountNotFound] = useState(false); + /* + Reconstruct the tx from xdr as passing a tx through extension contexts + loses custom prototypes associated with some values. This is fine for most cases + where we just need a high level overview of the tx, like just a list of operations. + But in this case, we will need the hostFn prototype associated with Soroban tx operations. + */ + + const SDK = isExperimentalModeEnabled ? SorobanSdk : StellarSdk; + const transaction = SDK.TransactionBuilder.fromXDR( + transactionXdr, + networkPassphrase, + ); - const allAccounts = useSelector(allAccountsSelector); - const publicKey = useSelector(publicKeySelector); - const hasPrivateKey = useSelector(hasPrivateKeySelector); + const { + _fee, + _innerTransaction, + _memo, + _networkPassphrase, + _sequence, + } = transaction; - // the public key the user had selected before starting this flow - const defaultPublicKey = useRef(publicKey); - const allAccountsMap = useRef({} as { [key: string]: Account }); - const accountToSign = blobOrTx.accountToSign; // both types have this key + const isFeeBump = !!_innerTransaction; + const memo = decodeMemo(_memo); + let accountToSign = _accountToSign; - const rejectAndClose = () => { - const _reject = isBlob ? rejectTransaction : rejectBlob; - dispatch(_reject()); - window.close(); - }; + const { + allAccounts, + accountNotFound, + currentAccount, + isConfirming, + isPasswordRequired, + publicKey, + handleApprove, + hwStatus, + rejectAndClose, + setIsPasswordRequired, + verifyPasswordThenSign, + } = useSetupSigningFlow( + rejectTransaction, + signTransaction, + transactionXdr, + accountToSign, + ); - const handleApprove = (signAndClose: () => Promise) => async () => { - setIsConfirming(true); + const flaggedKeyValues = Object.values(flaggedKeys as FlaggedKeys); + const isUnsafe = flaggedKeyValues.some(({ tags }) => + tags.includes(TRANSACTION_WARNING.unsafe), + ); + const isMalicious = flaggedKeyValues.some(({ tags }) => + tags.includes(TRANSACTION_WARNING.malicious), + ); + const isMemoRequired = flaggedKeyValues.some( + ({ tags }) => tags.includes(TRANSACTION_WARNING.memoRequired) && !memo, + ); - if (hasPrivateKey) { - await signAndClose(); - } else { - setIsPasswordRequired(true); + const resolveFederatedAddress = useCallback(async (inputDest) => { + let resolvedPublicKey; + try { + const fedResp = await FederationServer.resolve(inputDest); + resolvedPublicKey = fedResp.account_id; + } catch (e) { + console.error(e); } - setIsConfirming(false); - }; + return resolvedPublicKey; + }, []); - useEffect(() => { - if (startedHwSign && hwStatus === ShowOverlayStatus.IDLE) { - window.close(); + const decodeAccountToSign = async () => { + if (_accountToSign) { + if (isMuxedAccount(_accountToSign)) { + const mAccount = MuxedAccount.fromAddress(_accountToSign, "0"); + accountToSign = mAccount.baseAccount().accountId(); + } + if (isFederationAddress(_accountToSign)) { + accountToSign = (await resolveFederatedAddress( + accountToSign, + )) as string; + } } - }, [startedHwSign, hwStatus]); + }; + decodeAccountToSign(); useEffect(() => { - // handle auto selecting the right account based on `accountToSign` - let autoSelectedAccountDetails; - - allAccounts.forEach((account) => { - if (accountToSign) { - // does the user have the `accountToSign` somewhere in the accounts list? - if (account.publicKey === accountToSign) { - // if the `accountToSign` is found, but it isn't active, make it active - if (defaultPublicKey.current !== account.publicKey) { - dispatch(makeAccountActive(account.publicKey)); - } - - // save the details of the `accountToSign` - autoSelectedAccountDetails = account; - } - } - - // create an object so we don't need to keep iterating over allAccounts when we switch accounts - allAccountsMap.current[account.publicKey] = account; - }); - - if (!autoSelectedAccountDetails) { - setAccountNotFound(true); + if (isMemoRequired) { + emitMetric(METRIC_NAMES.signTransactionMemoRequired); + } + if (isUnsafe) { + emitMetric(METRIC_NAMES.signTransactionUnsafe); + } + if (isMalicious) { + emitMetric(METRIC_NAMES.signTransactionMalicious); } - }, [accountToSign, allAccounts, dispatch]); + }, [isMemoRequired, isMalicious, isUnsafe]); - useEffect(() => { - // handle any changes to the current acct - whether by auto select or manual select - setCurrentAccount(allAccountsMap.current[publicKey] || ({} as Account)); - }, [allAccounts, publicKey]); + const isSubmitDisabled = isMemoRequired || isMalicious; + + if (_networkPassphrase !== networkPassphrase) { + return ( + + window.close()} + isActive + header={`${t("Freighter is set to")} ${networkName}`} + > +

    + {t("The transaction you’re trying to sign is on")}{" "} + {_networkPassphrase}. +

    +

    {t("Signing this transaction is not possible at the moment.")}

    +
    +
    + ); + } - if ("blob" in blobOrTx) { + if (!isHttpsDomain && !isExperimentalModeEnabled) { return ( - + + window.close()} + isActive + variant={WarningMessageVariant.warning} + header={t("WEBSITE CONNECTION IS NOT SECURE")} + > +

    + + The website {{ domain }} does not use an SSL + certificate. For additional safety Freighter only works with + websites that provide an SSL certificate. + +

    +
    +
    ); } - return ( - setIsPasswordRequired(false)} + customSubmit={verifyPasswordThenSign} /> + ) : ( + <> + {hwStatus === ShowOverlayStatus.IN_PROGRESS && } +
    + + + {t("Confirm Transaction")} + + {isExperimentalModeEnabled ? ( + +

    + {t( + "You are interacting with a transaction that may be using untested and changing schemas. Proceed at your own risk.", + )} +

    +
    + ) : null} + {flaggedKeyValues.length ? ( + + ) : null} + {!isDomainListedAllowed && !isSubmitDisabled ? ( + + ) : null} +
    + + +
    + {t("is requesting approval to a")}{" "} + {isFeeBump ? "fee bump " : ""} + {t("transaction")}: +
    +
    +
    + {t("Approve using")}: +
    +
    setIsDropdownOpen(true)} + > + + + +
    + +
    +
    +
    +
    + {accountNotFound && accountToSign ? ( +
    + } + title={t("Account not available")} + > + {t("The application is requesting a specific account")} ( + {truncatedPublicKey(accountToSign)}),{" "} + {t( + "which is not available on Freighter. If you own this account, you can import it into Freighter to complete this request.", + )} + +
    + ) : null} +
    + {isFeeBump ? ( +
    + +
    + ) : ( + + )} + {t("Transaction Info")} + +
    + + + + + +
    + +
    +
    +
    + ); }; diff --git a/extension/src/popup/views/SignTransaction/styles.scss b/extension/src/popup/views/SignTransaction/styles.scss index a20f16939e..0b14a73124 100644 --- a/extension/src/popup/views/SignTransaction/styles.scss +++ b/extension/src/popup/views/SignTransaction/styles.scss @@ -1,5 +1,4 @@ -.SignTransaction, -.SignBlob { +.SignTransaction { height: var(--popup--height); overflow: hidden; position: relative; diff --git a/extension/src/popup/views/__tests__/SendTokenPayment.test.tsx b/extension/src/popup/views/__tests__/SendTokenPayment.test.tsx new file mode 100644 index 0000000000..38114680b3 --- /dev/null +++ b/extension/src/popup/views/__tests__/SendTokenPayment.test.tsx @@ -0,0 +1,188 @@ +import React from "react"; +import { render, waitFor, screen, fireEvent } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { createMemoryHistory } from "history"; +import { + TESTNET_NETWORK_DETAILS, + DEFAULT_NETWORKS, +} from "@shared/constants/stellar"; + +import { + Wrapper, + mockBalances, + mockAccounts, + mockTokenBalance, + mockTokenBalances, +} from "../../__testHelpers__"; +import * as ApiInternal from "@shared/api/internal"; +import * as UseNetworkFees from "popup/helpers/useNetworkFees"; + +import { APPLICATION_STATE as ApplicationState } from "@shared/constants/applicationState"; +import { ROUTES } from "popup/constants/routes"; +import { SendPayment } from "popup/views/SendPayment"; +import { initialState as transactionSubmissionInitialState } from "popup/ducks/transactionSubmission"; +import { initialState as sorobanInitialState } from "popup/ducks/soroban"; + +jest.spyOn(ApiInternal, "getAccountBalances").mockImplementation(() => { + return Promise.resolve(mockBalances); +}); + +jest.spyOn(ApiInternal, "getSorobanTokenBalance").mockImplementation(() => { + return Promise.resolve(mockTokenBalance); +}); + +jest.mock("popup/ducks/soroban", () => { + const original = jest.requireActual("popup/ducks/soroban"); + return { + ...original, + getTokenBalances: () => { + return { + type: "test-action", + payload: mockTokenBalances, + }; + }, + }; +}); + +jest + .spyOn(ApiInternal, "signFreighterSorobanTransaction") + .mockImplementation(() => { + return Promise.resolve({ + signedTransaction: + "AAAAAgAAAACM6IR9GHiRoVVAO78JJNksy2fKDQNs2jBn8bacsRLcrAAAAGQAALDTAAAAmQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAACAAAAEgAAAAGvUaDMj6075hfTiVH7DPAwLD7vh/GD+dlkZfp6o9gqdgAAAA8AAAAGc3ltYm9sAAAAAAAAAAAAAAAAAAA=", + }); + }); + +jest + .spyOn(ApiInternal, "submitFreighterSorobanTransaction") + .mockImplementation(() => { + return Promise.resolve({ + status: "PENDING", + hash: "some-hash", + latestLedger: 32131, + latestLedgerCloseTime: 62131, + }); + }); + +jest.spyOn(UseNetworkFees, "useNetworkFees").mockImplementation(() => { + return { + recommendedFee: ".00001", + networkCongestion: UseNetworkFees.NetworkCongestion.MEDIUM, + }; +}); + +jest.mock("soroban-client", () => { + const original = jest.requireActual("soroban-client"); + return { + ...original, + Server: class { + prepareTransaction(tx: any, _passphrase: string) { + return Promise.resolve(tx as any); + } + loadAccount() { + return { + sequenceNumber: () => 1, + accountId: () => publicKey, + incrementSequenceNumber: () => {}, + }; + } + }, + }; +}); + +jest.mock("react-router-dom", () => { + const ReactRouter = jest.requireActual("react-router-dom"); + return { + ...ReactRouter, + Redirect: ({ to }: any) =>
    redirect {to}
    , + }; +}); +const mockHistoryGetter = jest.fn(); +jest.mock("popup/constants/history", () => ({ + get history() { + return mockHistoryGetter(); + }, +})); + +const publicKey = "GA4UFF2WJM7KHHG4R5D5D2MZQ6FWMDOSVITVF7C5OLD5NFP6RBBW2FGV"; + +describe("SendTokenPayment", () => { + const history = createMemoryHistory(); + history.push(ROUTES.sendPaymentTo); + mockHistoryGetter.mockReturnValue(history); + + const asset = "DT:CCXVDIGMR6WTXZQX2OEVD6YM6AYCYPXPQ7YYH6OZMRS7U6VD3AVHNGBJ"; + const { container } = render( + + + , + ); + + it("can send a payment using a Soroban token", async () => { + await waitFor(async () => { + const input = screen.getByTestId("send-to-input"); + await userEvent.type(input, publicKey); + }); + + await waitFor( + async () => { + const continueBtn = screen.getByTestId("send-to-btn-continue"); + fireEvent.click(continueBtn); + }, + { timeout: 3000 }, + ); + + await waitFor(async () => { + const input = screen.getByTestId("send-amount-amount-input"); + fireEvent.change(input, { target: { value: "5" } }); + }); + + await waitFor(async () => { + const continueBtn = screen.getByTestId("send-amount-btn-continue"); + expect(continueBtn).not.toBeDisabled(); + await fireEvent.click(continueBtn); + }); + + await waitFor(async () => { + screen.getByTestId("send-settings-view"); + const continueBtn = screen.getByTestId("send-settings-btn-continue"); + await fireEvent.click(continueBtn); + }); + + await waitFor(async () => { + expect(container).toHaveTextContent("5 DT"); + const sendBtn = screen.getByTestId("transaction-details-btn-send"); + await fireEvent.click(sendBtn); + }); + + await waitFor(() => screen.getByTestId("submit-success-view")); + }); +}); diff --git a/yarn.lock b/yarn.lock index cc34ea2c51..9d30028f27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4521,6 +4521,11 @@ resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== +"@types/semver@^7.5.2": + version "7.5.2" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.2.tgz#31f6eec1ed7ec23f4f05608d3a2d381df041f564" + integrity sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw== + "@types/serve-index@^1.9.1": version "1.9.1" resolved "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz" @@ -5397,16 +5402,6 @@ asap@~2.0.3: resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= -asn1.js@^5.2.0: - version "5.4.1" - resolved "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz" - integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" - ast-types-flow@^0.0.7: version "0.0.7" resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz" @@ -5690,6 +5685,11 @@ bignumber.js@^9.1.1: resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz" integrity sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig== +bignumber.js@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + bin-links@4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/bin-links/-/bin-links-4.0.1.tgz#afeb0549e642f61ff889b58ea2f8dca78fb9d8d3" @@ -5728,16 +5728,6 @@ bluebird@^3.5.5: resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: - version "4.11.9" - resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz" - integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== - -bn.js@^5.1.1: - version "5.1.3" - resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz" - integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== - body-parser@1.20.1: version "1.20.1" resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz" @@ -5853,70 +5843,11 @@ broccoli-plugin@^4.0.7: rimraf "^3.0.2" symlink-or-copy "^1.3.1" -brorand@^1.0.1: - version "1.1.0" - resolved "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= - browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.2.0" - resolved "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz" - integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= - dependencies: - bn.js "^4.1.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.1" - resolved "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz" - integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== - dependencies: - bn.js "^5.1.1" - browserify-rsa "^4.0.1" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.3" - inherits "^2.0.4" - parse-asn1 "^5.1.5" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.8.5: version "4.14.5" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.14.5.tgz" @@ -5975,11 +5906,6 @@ buffer-from@^1.0.0: resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz" - integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= - buffer@^5.1.0: version "5.7.0" resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.0.tgz" @@ -6299,7 +6225,7 @@ ci-info@^3.2.0: resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz" integrity sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg== -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: +cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" resolved "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz" integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== @@ -6871,20 +6797,7 @@ crc@^3.5.0: dependencies: buffer "^5.1.0" -crc@^4.3.2: - version "4.3.2" - resolved "https://registry.npmjs.org/crc/-/crc-4.3.2.tgz" - integrity sha512-uGDHf4KLLh2zsHa8D8hIQ1H/HtFQhyHrc0uhHBcoKGol/Xnb+MPYfUMw7cvON6ze/GUESTudKayDcJC5HnJv1A== - -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: +create-hash@^1.1.0, create-hash@^1.1.2: version "1.2.0" resolved "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz" integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== @@ -6895,7 +6808,7 @@ create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: ripemd160 "^2.0.1" sha.js "^2.4.0" -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: +create-hmac@^1.1.4, create-hmac@^1.1.7: version "1.1.7" resolved "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz" integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== @@ -6934,23 +6847,6 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypto-browserify@^3.12.0: - version "3.12.0" - resolved "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - crypto-random-string@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz" @@ -7361,14 +7257,6 @@ depd@~1.1.2: resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= -des.js@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz" - integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - destroy@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" @@ -7417,15 +7305,6 @@ diff-sequences@^28.1.1: resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz" integrity sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw== -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - dir-glob@^2.0.0: version "2.2.2" resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz" @@ -7692,19 +7571,6 @@ electron-to-chromium@^1.4.284: resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz" integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== -elliptic@^6.5.3: - version "6.5.3" - resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz" - integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== - dependencies: - bn.js "^4.4.0" - brorand "^1.0.1" - hash.js "^1.0.0" - hmac-drbg "^1.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.0" - emittery@^0.10.2: version "0.10.2" resolved "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz" @@ -8244,19 +8110,6 @@ eventsource@^1.1.1: resolved "https://registry.npmjs.org/eventsource/-/eventsource-1.1.2.tgz" integrity sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA== -eventsource@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz" - integrity sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA== - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - execa@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/execa/-/execa-2.1.0.tgz" @@ -9212,14 +9065,6 @@ hash-base@^3.0.0: readable-stream "^3.6.0" safe-buffer "^5.2.0" -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - hast-to-hyperscript@^9.0.0: version "9.0.0" resolved "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-9.0.0.tgz" @@ -9327,15 +9172,6 @@ history@^4, history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" -hmac-drbg@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" @@ -11533,14 +11369,6 @@ micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - mime-db@1.44.0: version "1.44.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz" @@ -11631,16 +11459,11 @@ mini-css-extract-plugin@^2.6.1: dependencies: schema-utils "^4.0.0" -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: +minimalistic-assert@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= - minimatch@3.0.4, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" @@ -12385,17 +12208,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-asn1@^5.0.0, parse-asn1@^5.1.5: - version "5.1.6" - resolved "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz" - integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== - dependencies: - asn1.js "^5.2.0" - browserify-aes "^1.0.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" - parse-entities@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz" @@ -12611,7 +12423,7 @@ path-type@^4.0.0: resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pbkdf2@^3.0.3, pbkdf2@^3.0.9: +pbkdf2@^3.0.9: version "3.1.1" resolved "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz" integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== @@ -13217,18 +13029,6 @@ psl@^1.1.7: resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - pump@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz" @@ -13334,21 +13134,13 @@ quick-temp@^0.1.8: rimraf "^2.5.4" underscore.string "~3.3.4" -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: +randombytes@^2.0.1, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - range-parser@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz" @@ -14228,12 +14020,12 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -14421,6 +14213,13 @@ semver@^7.3.5: dependencies: lru-cache "^6.0.0" +semver@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + send@0.18.0: version "0.18.0" resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz" @@ -14768,21 +14567,15 @@ sodium-native@^4.0.1: dependencies: node-gyp-build "^4.6.0" -soroban-client@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/soroban-client/-/soroban-client-0.9.1.tgz#aeaf19ccb8258661dd9b5821090ea1b6c5960dae" - integrity sha512-wAxu8Z15vBirjizdGG5BnUtZDtf/+ul37q84xHqUUHS7t7sL8CBeTdM6bFYmlEVLjpfFtAhdB/yJBQU7Bo4kBQ== +soroban-client@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/soroban-client/-/soroban-client-1.0.0-beta.2.tgz#a59f9bd88436d6f06f38213efc4547676bee70d1" + integrity sha512-v5h3yvef7HkUD3H26w33NUEgRXcPiOSDWEsVzMloaxsprs3N002tXJHvFF+Uw1eYt50Uk6bvqBgvkLwX10VENw== dependencies: axios "^1.4.0" bignumber.js "^9.1.1" buffer "^6.0.3" - detect-node "^2.0.4" - es6-promise "^4.2.4" - eventsource "^2.0.2" - lodash "^4.17.21" - randombytes "^2.1.0" - stellar-base "10.0.0-soroban.3" - toml "^3.0.0" + stellar-base v10.0.0-beta.1 urijs "^1.19.1" sort-css-media-queries@2.1.0: @@ -14969,22 +14762,6 @@ std-env@^3.0.1: resolved "https://registry.npmjs.org/std-env/-/std-env-3.3.2.tgz" integrity sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA== -stellar-base@10.0.0-soroban.3: - version "10.0.0-soroban.3" - resolved "https://registry.yarnpkg.com/stellar-base/-/stellar-base-10.0.0-soroban.3.tgz#76dade3cf9ed8969a8d9cc37c78c8a61406b2470" - integrity sha512-XixwHHggxuWHQYY2CiVIzXLCGwaphS6pKF1GFyTC4QNIouR7IRWn/fzbXkYmzIbJTq6gM4mORO3kaLwl4bd7yA== - dependencies: - base32.js "^0.1.0" - bignumber.js "^9.1.1" - buffer "^6.0.3" - crc "^4.3.2" - crypto-browserify "^3.12.0" - js-xdr "^3.0.0" - sha.js "^2.3.6" - tweetnacl "^1.0.3" - optionalDependencies: - sodium-native "^4.0.1" - stellar-base@^0.13.1: version "0.13.2" resolved "https://registry.npmjs.org/stellar-base/-/stellar-base-0.13.2.tgz" @@ -15015,6 +14792,20 @@ stellar-base@^8.2.2: optionalDependencies: sodium-native "^3.3.0" +stellar-base@v10.0.0-beta.1: + version "10.0.0-beta.1" + resolved "https://registry.yarnpkg.com/stellar-base/-/stellar-base-10.0.0-beta.1.tgz#5b4209fbc44b8af82dd3ee8b7f6f4397126d8c3b" + integrity sha512-zXC5AsbUsLi57JruyeIMv23s3iUxq/P2ZFrSJ+FerLIZjSAjY8EDs4zwY4LCuu7swUu46Lm8GK6sqxUZCPekHw== + dependencies: + base32.js "^0.1.0" + bignumber.js "^9.1.2" + buffer "^6.0.3" + js-xdr "^3.0.0" + sha.js "^2.3.6" + tweetnacl "^1.0.3" + optionalDependencies: + sodium-native "^4.0.1" + stellar-hd-wallet@^0.0.10: version "0.0.10" resolved "https://registry.npmjs.org/stellar-hd-wallet/-/stellar-hd-wallet-0.0.10.tgz" @@ -15603,11 +15394,6 @@ toml@^2.3.0: resolved "https://registry.npmjs.org/toml/-/toml-2.3.6.tgz" integrity sha512-gVweAectJU3ebq//Ferr2JUY4WKSDe5N+z0FvjDncLGyHmIDoxgY/2Ie4qfEIDm4IS7OA6Rmdm7pdEEdMcV/xQ== -toml@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz" - integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== - toposort@^1.0.0: version "1.0.7" resolved "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz"