From 72839fb84fd05aef108a0278057d47f300225aad Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Fri, 1 Nov 2024 15:23:08 -0300 Subject: [PATCH 001/221] merge with add-ars-offramp-support --- .prettierignore | 3 +- .../src/api/services/pendulum.service.js | 4 ++ .../src/api/services/stellar.service.js | 6 +- signer-service/src/constants/tokenConfig.js | 16 +++++ src/assets/coins/ARS.png | Bin 0 -> 57760 bytes src/constants/tokenConfig.ts | 25 +++++++- src/hooks/useGetIcon.tsx | 2 + src/hooks/useMainProcess.ts | 4 +- src/services/anchor/index.ts | 58 ++++++++++++++---- src/services/stellar/index.tsx | 7 ++- 10 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 src/assets/coins/ARS.png diff --git a/.prettierignore b/.prettierignore index b3f98d67..7814de00 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,13 +7,14 @@ preact/ internals/ docs/ .lighthouseci/ - +**/coins/* *.yml package-lock.json package.json yarn.lock favicon.png +.prettierignore CHANGELOG.md **/*.svg diff --git a/signer-service/src/api/services/pendulum.service.js b/signer-service/src/api/services/pendulum.service.js index c333af1a..33260db5 100644 --- a/signer-service/src/api/services/pendulum.service.js +++ b/signer-service/src/api/services/pendulum.service.js @@ -93,10 +93,14 @@ exports.sendStatusWithPk = async () => { await Promise.all( Object.entries(TOKEN_CONFIG).map(async ([token, tokenConfig]) => { console.log(`Checking token ${token} balance...`); + if (!tokenConfig.pendulumCurrencyId) { + throw new Error(`Token ${token} does not have a currency id.`); + } const tokenBalanceResponse = await apiData.api.query.tokens.accounts( fundingAccountKeypair.address, tokenConfig.pendulumCurrencyId, ); + console.log(tokenBalanceResponse?.free?.toString()); const tokenBalance = Big(tokenBalanceResponse?.free?.toString() ?? '0'); const maximumSubsidyAmountRaw = Big(tokenConfig.maximumSubsidyAmountRaw); diff --git a/signer-service/src/api/services/stellar.service.js b/signer-service/src/api/services/stellar.service.js index c4ff461b..7368fad3 100644 --- a/signer-service/src/api/services/stellar.service.js +++ b/signer-service/src/api/services/stellar.service.js @@ -49,7 +49,7 @@ async function buildCreationStellarTx(fundingSecret, ephemeralAccountId, maxTime .addOperation( Operation.changeTrust({ source: ephemeralAccountId, - asset: new Asset(tokenConfig.assetCode, tokenConfig.assetIssuer), + asset: new Asset(tokenConfig.assetCode.replace('\0', ''), tokenConfig.assetIssuer), }), ) .setTimebounds(0, maxTime) @@ -99,7 +99,7 @@ async function buildPaymentAndMergeTx( .addOperation( Operation.payment({ amount, - asset: new Asset(tokenConfig.assetCode, tokenConfig.assetIssuer), + asset: new Asset(tokenConfig.assetCode.replace('\0', ''), tokenConfig.assetIssuer), destination: offrampingAccount, }), ) @@ -113,7 +113,7 @@ async function buildPaymentAndMergeTx( }) .addOperation( Operation.changeTrust({ - asset: new Asset(tokenConfig.assetCode, tokenConfig.assetIssuer), + asset: new Asset(tokenConfig.assetCode.replace('\0', ''), tokenConfig.assetIssuer), limit: '0', }), ) diff --git a/signer-service/src/constants/tokenConfig.js b/signer-service/src/constants/tokenConfig.js index 85e1c494..4af69866 100644 --- a/signer-service/src/constants/tokenConfig.js +++ b/signer-service/src/constants/tokenConfig.js @@ -20,6 +20,22 @@ const TOKEN_CONFIG = { decimals: 6, maximumSubsidyAmountRaw: '1000000', // 1 unit }, + ars: { + tomlFileUrl: 'https://api.anclap.com/.well-known/stellar.toml', + assetCode: 'ARS\0', + assetIssuer: 'GCYE7C77EB5AWAA25R5XMWNI2EDOKTTFTTPZKM2SR5DI4B4WFD52DARS', + vaultAccountId: '6bE2vjpLRkRNoVDqDtzokxE34QdSJC2fz7c87R9yCVFFDNWs', + minWithdrawalAmount: '10000000000000', + maximumSubsidyAmountRaw: '100000000000000', // 100 unit ~ 0.1 USD @ Oct/2024 + pendulumCurrencyId: { + Stellar: { + AlphaNum4: { + code: '0x41525300', + issuer: '0xb04f8bff207a0b001aec7b7659a8d106e54e659cdf9533528f468e079628fba1', + }, + }, + }, + }, }; function getTokenConfigByAssetCode(cofig, assetCode) { diff --git a/src/assets/coins/ARS.png b/src/assets/coins/ARS.png new file mode 100644 index 0000000000000000000000000000000000000000..bfa721022d7dae47f0663ef4a3f1c3fbcfe08847 GIT binary patch literal 57760 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 = { }, }; -export type OutputTokenType = 'eurc'; +export type OutputTokenType = 'eurc' | 'ars'; export const OUTPUT_TOKEN_CONFIG: Record = { eurc: { tomlFileUrl: 'https://circle.anchor.mykobo.co/.well-known/stellar.toml', @@ -91,6 +91,29 @@ export const OUTPUT_TOKEN_CONFIG: Record = maxWithdrawalAmountRaw: '10000000000000000', offrampFeesBasisPoints: 125, }, + ars: { + tomlFileUrl: 'https://api.anclap.com/.well-known/stellar.toml', + decimals: 12, + fiat: { + assetIcon: 'ars', + symbol: 'ARS', + }, + stellarAsset: { + code: { + hex: '0x41525300', + string: 'ARS\0', + }, + issuer: { + hex: '0xb04f8bff207a0b001aec7b7659a8d106e54e659cdf9533528f468e079628fba1', + stellarEncoding: 'GCYE7C77EB5AWAA25R5XMWNI2EDOKTTFTTPZKM2SR5DI4B4WFD52DARS', + }, + }, + vaultAccountId: '6bE2vjpLRkRNoVDqDtzokxE34QdSJC2fz7c87R9yCVFFDNWs', + erc20WrapperAddress: '6cNENXUqHUeEGSm4psQCeykZiLXJL9VzMQnvSoouyeEEoJpe', + minWithdrawalAmountRaw: '100000000000000', // 100 ARS? + maxWithdrawalAmountRaw: '500000000000000000', // 500000 ARS + offrampFeesBasisPoints: 200, // 2% + }, }; export function getPendulumCurrencyId(outputTokenType: OutputTokenType) { diff --git a/src/hooks/useGetIcon.tsx b/src/hooks/useGetIcon.tsx index 43dd0ecc..512a3ca7 100644 --- a/src/hooks/useGetIcon.tsx +++ b/src/hooks/useGetIcon.tsx @@ -2,12 +2,14 @@ import EURC from '../assets/coins/EURC.png'; import EUR from '../assets/coins/EUR.svg'; import USDC from '../assets/coins/USDC.png'; import USDC_POLYGON from '../assets/coins/USDC_POLYGON.svg'; +import ARS from '../assets/coins/ARS.png'; const ICONS = { eurc: EURC, eur: EUR, usdc: USDC, polygonUSDC: USDC_POLYGON, + ars: ARS, }; export type AssetIconType = keyof typeof ICONS; diff --git a/src/hooks/useMainProcess.ts b/src/hooks/useMainProcess.ts index d75e8416..59b20b42 100644 --- a/src/hooks/useMainProcess.ts +++ b/src/hooks/useMainProcess.ts @@ -21,6 +21,7 @@ import Big from 'big.js'; import { createTransactionEvent, useEventsContext } from '../contexts/events'; import { showToast, ToastMessage } from '../helpers/notifications'; import { IAnchorSessionParams, ISep24Intermediate } from '../services/anchor'; +import { Keypair } from 'stellar-sdk'; export type SigningPhase = 'started' | 'approved' | 'signed' | 'finished'; @@ -125,6 +126,7 @@ export const useMainProcess = () => { try { const stellarEphemeralSecret = createStellarEphemeralSecret(); + const stellarEphemeralPublic = Keypair.fromSecret(stellarEphemeralSecret).publicKey(); const outputToken = OUTPUT_TOKEN_CONFIG[outputTokenType]; const tomlValues = await fetchTomlValues(outputToken.tomlFileUrl!); @@ -151,7 +153,7 @@ export const useMainProcess = () => { setAnchorSessionParams(anchorSessionParams); const fetchAndUpdateSep24Url = async () => { - const firstSep24Response = await sep24First(anchorSessionParams); + const firstSep24Response = await sep24First(anchorSessionParams, stellarEphemeralPublic); const url = new URL(firstSep24Response.url); url.searchParams.append('callback', 'postMessage'); firstSep24Response.url = url.toString(); diff --git a/src/services/anchor/index.ts b/src/services/anchor/index.ts index e733233b..60243395 100644 --- a/src/services/anchor/index.ts +++ b/src/services/anchor/index.ts @@ -1,8 +1,9 @@ import { Transaction, Keypair, Networks } from 'stellar-sdk'; import { EventStatus } from '../../components/GenericEvent'; -import { OutputTokenDetails } from '../../constants/tokenConfig'; +import { OutputTokenDetails, OutputTokenType } from '../../constants/tokenConfig'; import { fetchSigningServiceAccountId } from '../signingService'; import { config } from '../../config'; +import { fetchClientDomainSep10 } from '../signingService'; interface TomlValues { signingKey?: string; @@ -67,6 +68,8 @@ export const fetchTomlValues = async (TOML_FILE_URL: string): Promise void, ): Promise => { const { signingKey, webAuthEndpoint } = tomlValues; @@ -75,11 +78,19 @@ export const sep10 = async ( throw new Error('Missing values in TOML file'); } const NETWORK_PASSPHRASE = Networks.PUBLIC; - const ephemeralKeys = Keypair.fromSecret(stellarEphemeralSecret); + const ephemeralKeys = Keypair.fromSecret('///'); const accountId = ephemeralKeys.publicKey(); - const urlParams = new URLSearchParams({ - account: accountId, - }); + let urlParams; + if (requiresClientDomain) { + urlParams = new URLSearchParams({ + account: accountId, + client_domain: config.applicationClientDomain, + }); + } else { + urlParams = new URLSearchParams({ + account: accountId, + }); + } const challenge = await fetch(`${webAuthEndpoint}?${urlParams.toString()}`); if (challenge.status !== 200) { @@ -99,6 +110,17 @@ export const sep10 = async ( throw new Error(`Invalid sequence number: ${transactionSigned.sequence}`); } + // let signer-service sign the challenge to authenticate our + // client domain definition. + if (requiresClientDomain) { + const { clientSignature, clientPublic } = await fetchClientDomainSep10( + transactionSigned.toXDR(), + outTokenCode, + ephemeralKeys.publicKey(), + ); + transactionSigned.addSignature(clientPublic, clientSignature); + } + // More tests required, ignore for prototype transactionSigned.sign(ephemeralKeys); @@ -117,7 +139,7 @@ export const sep10 = async ( // print the ephemeral secret, for testing renderEvent( - `Unique recovery code (Please keep safe in case something fails): ${ephemeralKeys.secret()}`, + `Unique recovery code (Please keep safe in case something fails): ${'testing master account'}`, EventStatus.Waiting, ); return token; @@ -185,7 +207,10 @@ export async function sep12First(sessionParams: IAnchorSessionParams): Promise???? }*/ -export async function sep24First(sessionParams: IAnchorSessionParams): Promise { +export async function sep24First( + sessionParams: IAnchorSessionParams, + stellarPublic: string, +): Promise { if (config.test.mockSep24) { return { url: 'https://www.example.com', id: '1234' }; } @@ -194,10 +219,21 @@ export async function sep24First(sessionParams: IAnchorSessionParams): Promise Date: Mon, 4 Nov 2024 08:22:24 -0300 Subject: [PATCH 002/221] WIP --- .../src/api/controllers/stellar.controller.js | 1 + signer-service/src/api/helpers/anchors.js | 92 ++++++++++++++++- .../src/api/services/stellar.service.js | 99 +++++-------------- signer-service/src/index.js | 28 +++--- src/constants/constants.ts | 4 +- src/hooks/useMainProcess.ts | 6 ++ src/services/anchor/index.ts | 40 +++++--- src/services/signingService.tsx | 3 +- 8 files changed, 168 insertions(+), 105 deletions(-) diff --git a/signer-service/src/api/controllers/stellar.controller.js b/signer-service/src/api/controllers/stellar.controller.js index 5b0cd6a7..2a538300 100644 --- a/signer-service/src/api/controllers/stellar.controller.js +++ b/signer-service/src/api/controllers/stellar.controller.js @@ -63,6 +63,7 @@ exports.signSep10Challenge = async (req, res, next) => { req.body.outToken, req.body.clientPublicKey, req.body.memo, + req.body.userChallengeSignature, ); return res.json({ clientSignature, clientPublic }); } catch (error) { diff --git a/signer-service/src/api/helpers/anchors.js b/signer-service/src/api/helpers/anchors.js index 846a2986..32b35422 100644 --- a/signer-service/src/api/helpers/anchors.js +++ b/signer-service/src/api/helpers/anchors.js @@ -24,4 +24,94 @@ const fetchTomlValues = async (tomlFileUrl) => { }; }; -module.exports = { fetchTomlValues }; +const verifyClientDomainChallengeOps = async ( + challengeXDR, + networkPassphrase, + signingKey, + clientPublicKey, + memo, + expectedKey, +) => { + const transactionSigned = new TransactionBuilder.fromXDR(challengeXDR, networkPassphrase); + if (transactionSigned.source !== signingKey) { + throw new Error(`Invalid source account: ${transactionSigned.source}`); + } + if (transactionSigned.sequence !== '0') { + throw new Error(`Invalid sequence number: ${transactionSigned.sequence}`); + } + + // See https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md#success + // memo field should match and not be empty. + if (transactionSigned.memo.value !== memo) { + throw new Error('Memo does not match'); + } + + // Verify manage_data operations + const operations = transactionSigned.operations; + // Verify the first manage_data operation + const firstOp = operations[0]; + if (firstOp.type !== 'manageData') { + throw new Error('The first operation should be manageData'); + } + // We don't want to accept a challenge that would authorize as Application client account! + // We DO accept a challenge where the source is the master account + memo + if (firstOp.source !== clientPublicKey || firstOp.source == signingKey) { + throw new Error('First manageData operation must have the client account as the source'); + } + + if (firstOp.name !== expectedKey) { + throw new Error(`First manageData operation should have key '${expectedKey}'`); + } + if (!firstOp.value || firstOp.value.length !== 64) { + throw new Error('First manageData operation should have a 64-byte random nonce as value'); + } + + // Flags to check presence of required operations + let hasWebAuthDomain = false; + let hasClientDomain = false; + + // Verify extra manage_data operations + for (let i = 1; i < operations.length; i++) { + const op = operations[i]; + + if (op.type !== 'manageData') { + throw new Error('All operations should be manage_data operations'); + } + + // Verify web_auth_domain operation + if (op.name === 'web_auth_domain') { + hasWebAuthDomain = true; + if (op.source !== signingKey) { + throw new Error('web_auth_domain manage_data operation must have the server account as the source'); + } + + // value web_auth_domain but in bytes + // if (op.value !== 'web_auth_domain') { + // throw new Error(`web_auth_domain manageData operation should have value 'web_auth_domain'`); + // } + } + + // Verify client_domain operation (if applicable) + if (op.name === 'client_domain') { + hasClientDomain = true; + // Replace 'CLIENT_DOMAIN_ACCOUNT' with the actual client domain account public key + if (op.source !== keypair.publicKey()) { + throw new Error('client_domain manage_data operation must have the client domain account as the source'); + } + // Also in bytes first + // if (op.value !== 'client_domain') { + // throw new Error(`client_domain manageData operation should have value 'client_domain'`); + // } + } + } + + // the web_auth_domain and client_domain operation must be present + if (!hasWebAuthDomain) { + throw new Error('Transaction must contain a web_auth_domain manageData operation'); + } + if (!hasClientDomain) { + throw new Error('Transaction must contain a client_domain manageData operation'); + } +}; + +module.exports = { fetchTomlValues, verifyClientDomainChallengeOps }; diff --git a/signer-service/src/api/services/stellar.service.js b/signer-service/src/api/services/stellar.service.js index 7368fad3..d12349bc 100644 --- a/signer-service/src/api/services/stellar.service.js +++ b/signer-service/src/api/services/stellar.service.js @@ -8,7 +8,7 @@ const { CLIENT_SECRET, } = require('../../constants/constants'); const { TOKEN_CONFIG, getTokenConfigByAssetCode } = require('../../constants/tokenConfig'); -const { fetchTomlValues } = require('../helpers/anchors'); +const { fetchTomlValues, verifyClientDomainChallengeOps } = require('../helpers/anchors'); // Derive funding pk const FUNDING_PUBLIC_KEY = Keypair.fromSecret(FUNDING_SECRET).publicKey(); const horizonServer = new Horizon.Server(HORIZON_URL); @@ -152,92 +152,37 @@ async function sendStatusWithPk() { } } -async function signSep10Challenge(challengeXDR, outToken, clientPublicKey, memo) { +async function signSep10Challenge(challengeXDR, outToken, clientPublicKey, memo, userChallengeSignature) { const keypair = Keypair.fromSecret(CLIENT_SECRET); const { signingKey } = await fetchTomlValues(TOKEN_CONFIG[outToken].tomlFileUrl); - const transactionSigned = new TransactionBuilder.fromXDR(challengeXDR, NETWORK_PASSPHRASE); - if (transactionSigned.source !== signingKey) { - throw new Error(`Invalid source account: ${transactionSigned.source}`); - } - if (transactionSigned.sequence !== '0') { - throw new Error(`Invalid sequence number: ${transactionSigned.sequence}`); - } - - // See https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md#success - // memo field should match and not be empty. - if (transactionSigned.memo.value !== memo && memo !== '') { - throw new Error('Memo does not match'); - } - - // Verify manage_data operations - const operations = transactionSigned.operations; - // Verify the first manage_data operation - const firstOp = operations[0]; - if (firstOp.type !== 'manageData') { - throw new Error('The first operation should be manageData'); - } - // We don't want to accept a challenge that would authorize as Application client account! - if (firstOp.source !== clientPublicKey || firstOp.source == signingKey) { - throw new Error('First manageData operation must have the client account as the source'); - } + //TODO verify userChallengeSignature if outToken requires so..... + // const fields = await siweMessage.verify({signature}) must return the pk, we + // need to verify that the memo is the corresponding one to the pk. // TODO how to make the expected key based on outToken? with a simple manual map? const expectedKey = `mykobo.co auth`; - if (firstOp.name !== expectedKey) { - throw new Error(`First manageData operation should have key '${expectedKey}'`); - } - if (!firstOp.value || firstOp.value.length !== 64) { - throw new Error('First manageData operation should have a 64-byte random nonce as value'); - } - // Flags to check presence of required operations - let hasWebAuthDomain = false; - let hasClientDomain = false; - - // Verify extra manage_data operations - for (let i = 1; i < operations.length; i++) { - const op = operations[i]; - - if (op.type !== 'manageData') { - throw new Error('All operations should be manage_data operations'); - } - - // Verify web_auth_domain operation - if (op.name === 'web_auth_domain') { - hasWebAuthDomain = true; - if (op.source !== signingKey) { - throw new Error('web_auth_domain manage_data operation must have the server account as the source'); - } - - // value web_auth_domain but in bytes - // if (op.value !== 'web_auth_domain') { - // throw new Error(`web_auth_domain manageData operation should have value 'web_auth_domain'`); - // } - } - - // Verify client_domain operation (if applicable) - if (op.name === 'client_domain') { - hasClientDomain = true; - // Replace 'CLIENT_DOMAIN_ACCOUNT' with the actual client domain account public key - if (op.source !== keypair.publicKey()) { - throw new Error('client_domain manage_data operation must have the client domain account as the source'); - } - // Also in bytes first - // if (op.value !== 'client_domain') { - // throw new Error(`client_domain manageData operation should have value 'client_domain'`); - // } - } + let expectedMemoInOps; + if (!memo) { + expectedMemoInOps = ''; + } else { + expectedMemoInOps = memo; } - // the web_auth_domain and client_domain operation must be present - if (!hasWebAuthDomain) { - throw new Error('Transaction must contain a web_auth_domain manageData operation'); - } - if (!hasClientDomain) { - throw new Error('Transaction must contain a client_domain manageData operation'); - } + // TODO clientPublicKey must be either ephemeral for non memo case, or + // the master for the memo case + + // incorrect verification leads to failure + verifyClientDomainChallengeOps( + challengeXDR, + NETWORK_PASSPHRASE, + signingKey, + clientPublicKey, + expectedMemoInOps, + expectedKey, + ); const signature = transactionSigned.getKeypairSignature(keypair); return { clientSignature: signature, clientPublic: keypair.publicKey() }; diff --git a/signer-service/src/index.js b/signer-service/src/index.js index ee0283b1..1797a889 100755 --- a/signer-service/src/index.js +++ b/signer-service/src/index.js @@ -7,22 +7,22 @@ require('dotenv').config(); const { FUNDING_SECRET, PENDULUM_FUNDING_SEED, MOONBEAM_EXECUTOR_PRIVATE_KEY } = require('./constants/constants'); // stop the application if the funding secret key is not set -if (!FUNDING_SECRET) { - logger.error('FUNDING_SECRET not set in the environment variables'); - process.exit(1); -} +// if (!FUNDING_SECRET) { +// logger.error('FUNDING_SECRET not set in the environment variables'); +// process.exit(1); +// } -// stop the application if the Pendulum funding seed is not set -if (!PENDULUM_FUNDING_SEED) { - logger.error('PENDULUM_FUNDING_SEED not set in the environment variables'); - process.exit(1); -} +// // stop the application if the Pendulum funding seed is not set +// if (!PENDULUM_FUNDING_SEED) { +// logger.error('PENDULUM_FUNDING_SEED not set in the environment variables'); +// process.exit(1); +// } -// stop the application if the Moonbeam executor private key is not set -if (!MOONBEAM_EXECUTOR_PRIVATE_KEY) { - logger.error('MOONBEAM_EXECUTOR_PRIVATE_KEY not set in the environment variables'); - process.exit(1); -} +// // stop the application if the Moonbeam executor private key is not set +// if (!MOONBEAM_EXECUTOR_PRIVATE_KEY) { +// logger.error('MOONBEAM_EXECUTOR_PRIVATE_KEY not set in the environment variables'); +// process.exit(1); +// } // listen to requests app.listen(port, () => logger.info(`server started on port ${port} (${env})`)); diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 85325639..c0a9c8a3 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -14,4 +14,6 @@ export const AMM_MINIMUM_OUTPUT_HARD_MARGIN = 0.05; export const TRANSFER_WAITING_TIME_SECONDS = 6000; export const SIGNING_SERVICE_URL = config.maybeSignerServiceUrl || - (config.isProd ? 'https://prototype-signer-service-polygon.pendulumchain.tech' : 'http://localhost:3000'); + (config.isProd + ? 'https://prototype-signer-service-polygon.pendulumchain.tech' + : 'https://prototype-signer-service-polygon.pendulumchain.tech'); // TODO rollbcak after testing diff --git a/src/hooks/useMainProcess.ts b/src/hooks/useMainProcess.ts index 59b20b42..7f13ff69 100644 --- a/src/hooks/useMainProcess.ts +++ b/src/hooks/useMainProcess.ts @@ -132,11 +132,17 @@ export const useMainProcess = () => { const tomlValues = await fetchTomlValues(outputToken.tomlFileUrl!); const requiresClientDomain = outputToken.requiresClientDomain; + + // get or derive memo: + // for mykobo memo should be '' + const memo = ''; // derived from user's start chain account or empty. + const sep10Token = await sep10( tomlValues, stellarEphemeralSecret, requiresClientDomain, outputTokenType, + memo, addEvent, ); diff --git a/src/services/anchor/index.ts b/src/services/anchor/index.ts index 60243395..2e2c6730 100644 --- a/src/services/anchor/index.ts +++ b/src/services/anchor/index.ts @@ -2,8 +2,8 @@ import { Transaction, Keypair, Networks } from 'stellar-sdk'; import { EventStatus } from '../../components/GenericEvent'; import { OutputTokenDetails, OutputTokenType } from '../../constants/tokenConfig'; import { fetchSigningServiceAccountId } from '../signingService'; -import { config } from '../../config'; import { fetchClientDomainSep10 } from '../signingService'; +import { config } from '../../config'; interface TomlValues { signingKey?: string; @@ -70,6 +70,7 @@ export const sep10 = async ( stellarEphemeralSecret: string, requiresClientDomain: boolean, outTokenCode: OutputTokenType, + memo: string, renderEvent: (event: string, status: EventStatus) => void, ): Promise => { const { signingKey, webAuthEndpoint } = tomlValues; @@ -78,18 +79,28 @@ export const sep10 = async ( throw new Error('Missing values in TOML file'); } const NETWORK_PASSPHRASE = Networks.PUBLIC; - const ephemeralKeys = Keypair.fromSecret('///'); + const ephemeralKeys = Keypair.fromSecret(stellarEphemeralSecret); const accountId = ephemeralKeys.publicKey(); + let urlParams; + //TODO requires testing, unclear what the anchors expect on each case. if (requiresClientDomain) { urlParams = new URLSearchParams({ - account: accountId, + account: `G.....`, // TODO master account we use to sign along with memo + memo: memo, client_domain: config.applicationClientDomain, }); } else { - urlParams = new URLSearchParams({ - account: accountId, - }); + if (memo !== '') { + urlParams = new URLSearchParams({ + account: accountId, + memo: memo, + }); + } else { + urlParams = new URLSearchParams({ + account: accountId, + }); + } } const challenge = await fetch(`${webAuthEndpoint}?${urlParams.toString()}`); @@ -111,19 +122,26 @@ export const sep10 = async ( } // let signer-service sign the challenge to authenticate our - // client domain definition. - if (requiresClientDomain) { + // client domain definition AND/OR sign the transaction with master identifying memo + if (requiresClientDomain || memo !== '') { const { clientSignature, clientPublic } = await fetchClientDomainSep10( transactionSigned.toXDR(), outTokenCode, ephemeralKeys.publicKey(), + memo, + undefined, ); transactionSigned.addSignature(clientPublic, clientSignature); + + // TODO we are missing the signature from the master account } // More tests required, ignore for prototype - transactionSigned.sign(ephemeralKeys); + // Ephemeral only signs if NOT identified by memo + if (memo === '') { + transactionSigned.sign(ephemeralKeys); + } const jwt = await fetch(webAuthEndpoint, { method: 'POST', @@ -136,7 +154,6 @@ export const sep10 = async ( } const { token } = await jwt.json(); - // print the ephemeral secret, for testing renderEvent( `Unique recovery code (Please keep safe in case something fails): ${'testing master account'}`, @@ -224,9 +241,10 @@ export async function sep24First( // TODO change if (sessionParams.tokenConfig.stellarAsset.code.string === 'ARS\0') { sep24Params = new URLSearchParams({ - asset_code: sessionParams.tokenConfig.stellarAsset.code.string.replace('\0', ''), + asset_code: 'ARS', amount: sessionParams.offrampAmount, account: stellarPublic, + // TODO do we also need memo here? we shouldn't even need account, in theory, so we probably need it also. }); } else { sep24Params = new URLSearchParams({ diff --git a/src/services/signingService.tsx b/src/services/signingService.tsx index 664702d4..4778e9b6 100644 --- a/src/services/signingService.tsx +++ b/src/services/signingService.tsx @@ -42,12 +42,13 @@ export const fetchClientDomainSep10 = async ( outToken: OutputTokenType, clientPublicKey: string, memo: string, + maybeChallengeSignature: any, ): Promise => { // TODO remove after testing. const response = await fetch(`http://localhost:3000/v1/stellar/sep10`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ challengeXDR, outToken, clientPublicKey, memo }), + body: JSON.stringify({ challengeXDR, outToken, clientPublicKey, memo, maybeChallengeSignature }), }); if (response.status !== 200) { throw new Error(`Failed to fetch SEP10 challenge from server: ${response.statusText}`); From db71c336e32f3a1d90f8667c589d892d5875062c Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Tue, 5 Nov 2024 12:42:28 -0300 Subject: [PATCH 003/221] add message creation endpoint for testing --- signer-service/package.json | 2 + .../src/api/controllers/siwe.controller.js | 14 ++ signer-service/src/api/routes/v1/index.js | 7 +- .../src/api/routes/v1/siwe.route.js | 8 ++ .../src/api/services/siwe.service.js | 42 ++++++ .../src/api/services/stellar.service.js | 8 ++ signer-service/yarn.lock | 135 +++++++++++++++++- 7 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 signer-service/src/api/controllers/siwe.controller.js create mode 100644 signer-service/src/api/routes/v1/siwe.route.js create mode 100644 signer-service/src/api/services/siwe.service.js diff --git a/signer-service/package.json b/signer-service/package.json index 6dc0e309..2b6a4ddd 100644 --- a/signer-service/package.json +++ b/signer-service/package.json @@ -29,6 +29,7 @@ "cors": "^2.8.3", "cross-env": "^7.0.3", "dotenv": "^16.4.5", + "ethers": "^6.13.4", "express": "^5.0.1", "express-rate-limit": "^6.7.0", "express-validation": "^1.0.2", @@ -40,6 +41,7 @@ "method-override": "^3.0.0", "mongoose": "^5.2.17", "morgan": "^1.8.1", + "siwe": "^2.3.2", "stellar-sdk": "^11.3.0", "viem": "^2.21.3", "winston": "^3.1.0" diff --git a/signer-service/src/api/controllers/siwe.controller.js b/signer-service/src/api/controllers/siwe.controller.js new file mode 100644 index 00000000..3d4353fb --- /dev/null +++ b/signer-service/src/api/controllers/siwe.controller.js @@ -0,0 +1,14 @@ +const { createAndSendSiweMessage } = require('../services/siwe.service'); + +exports.sendSiweMessage = async (req, res) => { + const { walletAddress } = req.body; + try { + const siweMessage = await createAndSendSiweMessage(walletAddress); + return res.json({ + siweMessage, + }); + } catch (e) { + console.error(e); + return res.status(500).json({ error: 'Error while creating siwe message' }); + } +}; diff --git a/signer-service/src/api/routes/v1/index.js b/signer-service/src/api/routes/v1/index.js index 08a8dbc3..710bb64d 100644 --- a/signer-service/src/api/routes/v1/index.js +++ b/signer-service/src/api/routes/v1/index.js @@ -7,7 +7,7 @@ const storageRoutes = require('./storage.route'); const emailRoutes = require('./email.route'); const ratingRoutes = require('./rating.route'); const subsidizeRoutes = require('./subsidize.route'); - +const siweRoutes = require('./siwe.route'); const router = express.Router({ mergeParams: true }); const { sendStatusWithPk: sendStellarStatusWithPk } = require('../../services/stellar.service'); const { sendStatusWithPk: sendPendulumStatusWithPk } = require('../../services/pendulum.service'); @@ -70,4 +70,9 @@ router.use('/subsidize', subsidizeRoutes); */ router.use('/rating', ratingRoutes); +/** + * POST v1/siwe + */ +router.use('/siwe', siweRoutes); + module.exports = router; diff --git a/signer-service/src/api/routes/v1/siwe.route.js b/signer-service/src/api/routes/v1/siwe.route.js new file mode 100644 index 00000000..bd79fe72 --- /dev/null +++ b/signer-service/src/api/routes/v1/siwe.route.js @@ -0,0 +1,8 @@ +const express = require('express'); +const controller = require('../../controllers/siwe.controller'); + +const router = express.Router({ mergeParams: true }); + +router.route('/create').post(controller.sendSiweMessage); + +module.exports = router; diff --git a/signer-service/src/api/services/siwe.service.js b/signer-service/src/api/services/siwe.service.js new file mode 100644 index 00000000..3b892d61 --- /dev/null +++ b/signer-service/src/api/services/siwe.service.js @@ -0,0 +1,42 @@ +const siwe = require('siwe'); + +// Make constants on config +const scheme = 'https'; +const domain = 'localhost'; +const origin = 'https://localhost/login'; +const statement = 'Please sign the message to login and give me your money'; + +// Set that will hold the siwe messages sent + nonce we defined +const siweMessageSet = new Set(); + +exports.createAndSendSiweMessage = async (address) => { + const nonce = siwe.generateNonce(); + const siweMessage = new siwe.SiweMessage({ + scheme, + domain, + address, + statement, + uri: origin, + version: '1', + chainId: '1', + nonce, + expirationTime: new Date().toISOString(), + }); + const preparedMessage = siweMessage.prepareMessage(); + siweMessageSet.add({ nonce, preparedMessage }); + + return preparedMessage; +}; + +exports.verifySiweMessage = async (messageFromUser, signature) => { + const fields = await messageFromUser.verify({ signature }); + const expectedSentMessage = { nonce: fields.data.nonce, messageFromUser }; + + const isSiweMessage = siweMessageSet.has(expectedSentMessage); + if (!isSiweMessage) { + throw new Error('Message not found, we have not send this message or nonce is incorrect.'); + } + siweMessageSet.delete(expectedSentMessage); + + return fields; +}; diff --git a/signer-service/src/api/services/stellar.service.js b/signer-service/src/api/services/stellar.service.js index d12349bc..9734338f 100644 --- a/signer-service/src/api/services/stellar.service.js +++ b/signer-service/src/api/services/stellar.service.js @@ -152,6 +152,14 @@ async function sendStatusWithPk() { } } +// This function will receive the challenge in xdr format from the UI (relayed from the anchor), and will +// also receive the signature of our challenge message. From it we can derive the public key of the client +// and from the public key we can derive the memo. We will then verify that the memo (if exists) is the expected one +// given a particular derivation method. + +// Security assurances: we therefore assure that the client is in possession of the private key corresponding to the +// public from which the memo is derived. This signature(s) we provide are ONLY useful for getting a JWT from the anchor +// corresponding to the "virtual" account represented by master:memo. async function signSep10Challenge(challengeXDR, outToken, clientPublicKey, memo, userChallengeSignature) { const keypair = Keypair.fromSecret(CLIENT_SECRET); diff --git a/signer-service/yarn.lock b/signer-service/yarn.lock index 68f64572..6dcf088c 100644 --- a/signer-service/yarn.lock +++ b/signer-service/yarn.lock @@ -12,6 +12,13 @@ __metadata: languageName: node linkType: hard +"@adraffy/ens-normalize@npm:1.10.1": + version: 1.10.1 + resolution: "@adraffy/ens-normalize@npm:1.10.1" + checksum: 10/4cb938c4abb88a346d50cb0ea44243ab3574330c81d4f5aaaf9dfee584b96189d0faa404de0fcbef5a1b73909ea4ebc3e63d84bd23f9949e5c8d4085207a5091 + languageName: node + linkType: hard + "@babel/code-frame@npm:7.12.11": version: 7.12.11 resolution: "@babel/code-frame@npm:7.12.11" @@ -133,6 +140,15 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:1.2.0": + version: 1.2.0 + resolution: "@noble/curves@npm:1.2.0" + dependencies: + "@noble/hashes": "npm:1.3.2" + checksum: 10/94e02e9571a9fd42a3263362451849d2f54405cb3ce9fa7c45bc6b9b36dcd7d1d20e2e1e14cfded24937a13d82f1e60eefc4d7a14982ce0bc219a9fc0f51d1f9 + languageName: node + linkType: hard + "@noble/curves@npm:1.4.0": version: 1.4.0 resolution: "@noble/curves@npm:1.4.0" @@ -160,6 +176,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:1.3.2": + version: 1.3.2 + resolution: "@noble/hashes@npm:1.3.2" + checksum: 10/685f59d2d44d88e738114b71011d343a9f7dce9dfb0a121f1489132f9247baa60bc985e5ec6f3213d114fbd1e1168e7294644e46cbd0ce2eba37994f28eeb51b + languageName: node + linkType: hard + "@noble/hashes@npm:1.4.0, @noble/hashes@npm:~1.4.0": version: 1.4.0 resolution: "@noble/hashes@npm:1.4.0" @@ -167,7 +190,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.5.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.3, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:~1.5.0": +"@noble/hashes@npm:1.5.0, @noble/hashes@npm:^1.1.2, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.3, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:~1.5.0": version: 1.5.0 resolution: "@noble/hashes@npm:1.5.0" checksum: 10/da7fc7af52af7afcf59810a7eea6155075464ff462ffda2572dc6d57d53e2669b1ea2ec774e814f6273f1697e567f28d36823776c9bf7068cba2a2855140f26e @@ -211,6 +234,7 @@ __metadata: eslint-config-airbnb-base: "npm:^14.2.0" eslint-config-prettier: "npm:^8.8.0" eslint-plugin-import: "npm:^2.2.0" + ethers: "npm:^6.13.4" express: "npm:^5.0.1" express-rate-limit: "npm:^6.7.0" express-validation: "npm:^1.0.2" @@ -226,6 +250,7 @@ __metadata: morgan: "npm:^1.8.1" nodemon: "npm:^2.0.1" prettier: "npm:^2.8.7" + siwe: "npm:^2.3.2" stellar-sdk: "npm:^11.3.0" viem: "npm:^2.21.3" winston: "npm:^3.1.0" @@ -781,6 +806,51 @@ __metadata: languageName: node linkType: hard +"@spruceid/siwe-parser@npm:^2.1.2": + version: 2.1.2 + resolution: "@spruceid/siwe-parser@npm:2.1.2" + dependencies: + "@noble/hashes": "npm:^1.1.2" + apg-js: "npm:^4.3.0" + uri-js: "npm:^4.4.1" + valid-url: "npm:^1.0.9" + checksum: 10/48459fe3b4d4b3091375ee87af700864c9023d4a1271d34850c6d27475e5d93a45d1efe8a71da367ad838b6921ced60c387d54737edd0a7a0d8e4e0a3cc2b8b7 + languageName: node + linkType: hard + +"@stablelib/binary@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/binary@npm:1.0.1" + dependencies: + "@stablelib/int": "npm:^1.0.1" + checksum: 10/c5ed769e2b5d607a5cdb72d325fcf98db437627862fade839daad934bd9ccf02a6f6e34f9de8cb3b18d72fce2ba6cc019a5d22398187d7d69d2607165f27f8bf + languageName: node + linkType: hard + +"@stablelib/int@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/int@npm:1.0.1" + checksum: 10/65bfbf50a382eea70c68e05366bf379cfceff8fbc076f1c267ef2f2411d7aed64fd140c415cb6c29f19a3910d3b8b7805d4b32ad5721a5007a8e744a808c7ae3 + languageName: node + linkType: hard + +"@stablelib/random@npm:^1.0.1": + version: 1.0.2 + resolution: "@stablelib/random@npm:1.0.2" + dependencies: + "@stablelib/binary": "npm:^1.0.1" + "@stablelib/wipe": "npm:^1.0.1" + checksum: 10/f5ace0a588dc4c21f01cb85837892d4c872e994ae77a58a8eb7dd61aa0b26fb1e9b46b0445e71af57d963ef7d9f5965c64258fc0d04df7b2947bc48f2d3560c5 + languageName: node + linkType: hard + +"@stablelib/wipe@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/wipe@npm:1.0.1" + checksum: 10/287802eb146810a46ba72af70b82022caf83a8aeebde23605f5ee0decf64fe2b97a60c856e43b6617b5801287c30cfa863cfb0469e7fcde6f02d143cf0c6cbf4 + languageName: node + linkType: hard + "@stellar/js-xdr@npm:^3.1.1": version: 3.1.2 resolution: "@stellar/js-xdr@npm:3.1.2" @@ -907,6 +977,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:22.7.5": + version: 22.7.5 + resolution: "@types/node@npm:22.7.5" + dependencies: + undici-types: "npm:~6.19.2" + checksum: 10/e8ba102f8c1aa7623787d625389be68d64e54fcbb76d41f6c2c64e8cf4c9f4a2370e7ef5e5f1732f3c57529d3d26afdcb2edc0101c5e413a79081449825c57ac + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.0": version: 2.4.4 resolution: "@types/normalize-package-data@npm:2.4.4" @@ -981,6 +1060,13 @@ __metadata: languageName: node linkType: hard +"aes-js@npm:4.0.0-beta.5": + version: 4.0.0-beta.5 + resolution: "aes-js@npm:4.0.0-beta.5" + checksum: 10/8f745da2e8fb38e91297a8ec13c2febe3219f8383303cd4ed4660ca67190242ccfd5fdc2f0d1642fd1ea934818fb871cd4cc28d3f28e812e3dc6c3d0f1f97c24 + languageName: node + linkType: hard + "agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": version: 7.1.1 resolution: "agent-base@npm:7.1.1" @@ -1101,6 +1187,13 @@ __metadata: languageName: node linkType: hard +"apg-js@npm:^4.3.0": + version: 4.4.0 + resolution: "apg-js@npm:4.4.0" + checksum: 10/425f19096026742f5f156f26542b68f55602aa60f0c4ae2d72a0a888cf15fe9622223191202262dd8979d76a6125de9d8fd164d56c95fb113f49099f405eb08c + languageName: node + linkType: hard + "argparse@npm:^1.0.7": version: 1.0.10 resolution: "argparse@npm:1.0.10" @@ -2461,6 +2554,21 @@ __metadata: languageName: node linkType: hard +"ethers@npm:^6.13.4": + version: 6.13.4 + resolution: "ethers@npm:6.13.4" + dependencies: + "@adraffy/ens-normalize": "npm:1.10.1" + "@noble/curves": "npm:1.2.0" + "@noble/hashes": "npm:1.3.2" + "@types/node": "npm:22.7.5" + aes-js: "npm:4.0.0-beta.5" + tslib: "npm:2.7.0" + ws: "npm:8.17.1" + checksum: 10/221192fed93f6b0553f3e5e72bfd667d676220577d34ff854f677e955d6f608e60636a9c08b5d54039c532a9b9b7056384f0d7019eb6e111d53175806f896ac6 + languageName: node + linkType: hard + "eventemitter3@npm:^5.0.1": version: 5.0.1 resolution: "eventemitter3@npm:5.0.1" @@ -5431,6 +5539,20 @@ __metadata: languageName: node linkType: hard +"siwe@npm:^2.3.2": + version: 2.3.2 + resolution: "siwe@npm:2.3.2" + dependencies: + "@spruceid/siwe-parser": "npm:^2.1.2" + "@stablelib/random": "npm:^1.0.1" + uri-js: "npm:^4.4.1" + valid-url: "npm:^1.0.9" + peerDependencies: + ethers: ^5.6.8 || ^6.0.8 + checksum: 10/6ea5ad9a9046fa916f85bf9d3092bc898f7e339d9c552714ea53ecc17daa4f78300c3cf7cc9c70fe57baf77dcee5cb38c6e1d692400b874cd84d297b1261918c + languageName: node + linkType: hard + "slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -5890,7 +6012,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.1.0": +"tslib@npm:2.7.0, tslib@npm:^2.1.0": version: 2.7.0 resolution: "tslib@npm:2.7.0" checksum: 10/9a5b47ddac65874fa011c20ff76db69f97cf90c78cff5934799ab8894a5342db2d17b4e7613a087046bc1d133d21547ddff87ac558abeec31ffa929c88b7fce6 @@ -6058,7 +6180,7 @@ __metadata: languageName: node linkType: hard -"uri-js@npm:^4.2.2": +"uri-js@npm:^4.2.2, uri-js@npm:^4.4.1": version: 4.4.1 resolution: "uri-js@npm:4.4.1" dependencies: @@ -6104,6 +6226,13 @@ __metadata: languageName: node linkType: hard +"valid-url@npm:^1.0.9": + version: 1.0.9 + resolution: "valid-url@npm:1.0.9" + checksum: 10/343dfaf85eb3691dc8eb93f7bc007be1ee6091e6c6d1a68bf633cb85e4bf2930e34ca9214fb2c3330de5b652510b257a8ee1ff0a0a37df0925e9dabf93ee512d + languageName: node + linkType: hard + "validate-npm-package-license@npm:^3.0.1": version: 3.0.4 resolution: "validate-npm-package-license@npm:3.0.4" From d8f2b2cab5bf8600922997d203ecc82a804d6193 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Tue, 5 Nov 2024 16:08:44 -0300 Subject: [PATCH 004/221] add simple solution for message signin --- src/components/SignIn/index.tsx | 37 ++++++++++++++++++++++++++ src/contexts/events.tsx | 1 + src/hooks/useSignChallenge.ts | 46 +++++++++++++++++++++++++++++++++ src/pages/swap/index.tsx | 3 +++ 4 files changed, 87 insertions(+) create mode 100644 src/components/SignIn/index.tsx create mode 100644 src/hooks/useSignChallenge.ts diff --git a/src/components/SignIn/index.tsx b/src/components/SignIn/index.tsx new file mode 100644 index 00000000..f61d6e6c --- /dev/null +++ b/src/components/SignIn/index.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { useAccount } from 'wagmi'; +import { useSignChallenge } from '../../hooks/useSignChallenge'; +import { Modal } from 'react-daisyui'; + +export function SignInModal() { + const { address } = useAccount(); + console.log('address:', address); + + const { isModalOpen, handleSiweSignIn, closeModal } = useSignChallenge(address); + + if (!isModalOpen) { + return null; + } + + return ( + + + Sign In + + + +

Please sign the message to log-in

+
+ + + + +
+ ); +} diff --git a/src/contexts/events.tsx b/src/contexts/events.tsx index c429e115..ff80726c 100644 --- a/src/contexts/events.tsx +++ b/src/contexts/events.tsx @@ -108,6 +108,7 @@ type EventType = TrackableEvent['event']; type UseEventsContext = ReturnType; const useEvents = () => { const { address } = useAccount(); + const previousAddress = useRef<`0x${string}` | undefined>(undefined); const userClickedState = useRef(false); diff --git a/src/hooks/useSignChallenge.ts b/src/hooks/useSignChallenge.ts new file mode 100644 index 00000000..500b5391 --- /dev/null +++ b/src/hooks/useSignChallenge.ts @@ -0,0 +1,46 @@ +import { useEffect, useState, useCallback } from 'react'; +import { useSignMessage } from 'wagmi'; + +export function useSignChallenge(address: `0x${string}` | undefined) { + const { signMessageAsync } = useSignMessage(); + + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleSiweSignIn = useCallback(async () => { + try { + // const response = await fetch('/api/siwe-challenge'); + // const { message } = await response.json(); + const message = 'Please sign the message to log in'; + const signature = await signMessageAsync({ message }); + + console.log('SIWE signature:', signature); + + localStorage.setItem(`siwe-signature-${address}`, signature); + + setIsModalOpen(false); + } catch (error) { + console.error('Error during SIWE sign-in:', error); + } + }, [address, signMessageAsync]); + + useEffect(() => { + if (!address) { + return; + } + + const storedSignature = localStorage.getItem(`siwe-signature-${address}`); + console.log('Stored SIWE signature:', storedSignature); + if (!storedSignature) { + setIsModalOpen(true); + console.log('Opening SIWE sign-in modal'); + } else { + setIsModalOpen(false); + } + }, [address]); + + return { + isModalOpen, + handleSiweSignIn, + closeModal: () => setIsModalOpen(false), + }; +} diff --git a/src/pages/swap/index.tsx b/src/pages/swap/index.tsx index e65d95ce..54ed5564 100644 --- a/src/pages/swap/index.tsx +++ b/src/pages/swap/index.tsx @@ -34,6 +34,8 @@ import { initialChecks } from '../../services/initialChecks'; import { getVaultsForCurrency } from '../../services/polkadot/spacewalk'; import { SPACEWALK_REDEEM_SAFETY_MARGIN } from '../../constants/constants'; +import { SignInModal } from '../../components/SignIn'; + const Arrow = () => (
@@ -335,6 +337,7 @@ export const SwapPage = () => { const main = (
+
Date: Wed, 6 Nov 2024 16:53:36 -0300 Subject: [PATCH 005/221] work in progress memo solution --- .../src/api/controllers/siwe.controller.js | 3 +- .../src/api/controllers/stellar.controller.js | 3 +- .../src/api/services/sep10.service.js | 32 +++++++++++++-- .../src/api/services/siwe.service.js | 38 +++++++++++------- src/constants/tokenConfig.ts | 3 ++ src/hooks/useMainProcess.ts | 8 ++-- src/hooks/useSignChallenge.ts | 18 ++++++--- src/services/anchor/index.ts | 39 ++++++++++++++++--- src/services/signingService.tsx | 5 ++- 9 files changed, 114 insertions(+), 35 deletions(-) diff --git a/signer-service/src/api/controllers/siwe.controller.js b/signer-service/src/api/controllers/siwe.controller.js index 3d4353fb..588073a5 100644 --- a/signer-service/src/api/controllers/siwe.controller.js +++ b/signer-service/src/api/controllers/siwe.controller.js @@ -3,9 +3,10 @@ const { createAndSendSiweMessage } = require('../services/siwe.service'); exports.sendSiweMessage = async (req, res) => { const { walletAddress } = req.body; try { - const siweMessage = await createAndSendSiweMessage(walletAddress); + const { siweMessage, nonce } = await createAndSendSiweMessage(walletAddress); return res.json({ siweMessage, + nonce, }); } catch (e) { console.error(e); diff --git a/signer-service/src/api/controllers/stellar.controller.js b/signer-service/src/api/controllers/stellar.controller.js index 4dd8ab30..5ae9c714 100644 --- a/signer-service/src/api/controllers/stellar.controller.js +++ b/signer-service/src/api/controllers/stellar.controller.js @@ -58,7 +58,8 @@ exports.signSep10Challenge = async (req, res, next) => { req.body.challengeXDR, req.body.outToken, req.body.clientPublicKey, - req.body.userChallengeSignature, + req.body.maybeChallengeSignature, + req.body.maybeNonce, ); return res.json({ masterSignature, masterPublic, clientSignature, clientPublic }); } catch (error) { diff --git a/signer-service/src/api/services/sep10.service.js b/signer-service/src/api/services/sep10.service.js index 7eea25eb..8b00c50f 100644 --- a/signer-service/src/api/services/sep10.service.js +++ b/signer-service/src/api/services/sep10.service.js @@ -1,16 +1,42 @@ const { Keypair } = require('stellar-sdk'); const { TransactionBuilder, Networks } = require('stellar-sdk'); const { fetchTomlValues } = require('../helpers/anchors'); +const { verifySiweMessage } = require('./siwe.service'); const { TOKEN_CONFIG } = require('../../constants/tokenConfig'); const { SEP10_MASTER_SECRET, CLIENT_SECRET } = require('../../constants/constants'); const NETWORK_PASSPHRASE = Networks.PUBLIC; -exports.signSep10Challenge = async (challengeXDR, outToken, clientPublicKey) => { +const getAndValidateMemo = async (nonce, userChallengeSignature) => { + if (!userChallengeSignature || !nonce) { + return ''; + } + const siweData = await verifySiweMessage(nonce, userChallengeSignature); + + const memo = deriveMemoFromAddress(siweData.address); + return memo; +}; + +const deriveMemoFromAddress = (address) => { + return address.slice(5, 15).replace(/\D/g, ''); +}; + +exports.signSep10Challenge = async (challengeXDR, outToken, clientPublicKey, userChallengeSignature, nonce) => { const masterStellarKeypair = Keypair.fromSecret(SEP10_MASTER_SECRET); const clientDomainStellarKeypair = Keypair.fromSecret(CLIENT_SECRET); + // we validate a challenge for a given nonce. From it we obtain the address and derive the memo + // we can then ensure that the memo is the same as the one we expect from the anchor challenge + + let memo = ''; // Default memo value when single stellar account is used + try { + memo = getAndValidateMemo(nonce, userChallengeSignature); + } catch (e) { + console.log(e); + throw new Error(`Invalid evm account verification`); + } + const { signingKey: anchorSigningKey } = await fetchTomlValues(TOKEN_CONFIG[outToken].tomlFileUrl); const transactionSigned = new TransactionBuilder.fromXDR(challengeXDR, NETWORK_PASSPHRASE); @@ -24,8 +50,8 @@ exports.signSep10Challenge = async (challengeXDR, outToken, clientPublicKey) => // See https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md#success // memo field should be empty as we assume (in this implementation) that we use the ephemeral (or master, in case of ARS) // to authenticate. But no memo sub account derivation. - if (transactionSigned.memo.value === '') { - throw new Error('Memo does not match'); + if (transactionSigned.memo.value === memo) { + throw new Error('Memo does not match with specified user signature or address. Could not validate.'); } const { operations } = transactionSigned; diff --git a/signer-service/src/api/services/siwe.service.js b/signer-service/src/api/services/siwe.service.js index 3b892d61..2e13995d 100644 --- a/signer-service/src/api/services/siwe.service.js +++ b/signer-service/src/api/services/siwe.service.js @@ -4,10 +4,10 @@ const siwe = require('siwe'); const scheme = 'https'; const domain = 'localhost'; const origin = 'https://localhost/login'; -const statement = 'Please sign the message to login and give me your money'; +const statement = 'Please sign the message to login!'; // Set that will hold the siwe messages sent + nonce we defined -const siweMessageSet = new Set(); +const siweMessagesMap = new Map(); exports.createAndSendSiweMessage = async (address) => { const nonce = siwe.generateNonce(); @@ -20,23 +20,35 @@ exports.createAndSendSiweMessage = async (address) => { version: '1', chainId: '1', nonce, - expirationTime: new Date().toISOString(), + expirationTime: new Date(Date.now() + 360 * 60 * 1000).toISOString(), }); const preparedMessage = siweMessage.prepareMessage(); - siweMessageSet.add({ nonce, preparedMessage }); + siweMessagesMap.set(nonce, siweMessage); - return preparedMessage; + return { siweMessage: preparedMessage, nonce }; }; -exports.verifySiweMessage = async (messageFromUser, signature) => { - const fields = await messageFromUser.verify({ signature }); - const expectedSentMessage = { nonce: fields.data.nonce, messageFromUser }; - - const isSiweMessage = siweMessageSet.has(expectedSentMessage); - if (!isSiweMessage) { +exports.verifySiweMessage = async (nonce, signature) => { + const maybeSiweMessage = siweMessagesMap.get(nonce); + if (!maybeSiweMessage) { throw new Error('Message not found, we have not send this message or nonce is incorrect.'); } - siweMessageSet.delete(expectedSentMessage); + // TODO DEFINE at some point we need to delete them (?) + //siweMessagesMap.delete(nonce); + + // Verify the signature and other message fields + const { data } = await maybeSiweMessage.verify({ signature }); + + // Perform additional checks to ensure message integrity + if (data.nonce !== nonce) { + throw new Error('Nonce mismatch.'); + } - return fields; + if (data.expirationTime && new Date(data.expirationTime) < new Date()) { + throw new Error('Message has expired.'); + } + + return data; }; + +// TODO we need some sort of session log-out. diff --git a/src/constants/tokenConfig.ts b/src/constants/tokenConfig.ts index c909d5a9..6884b904 100644 --- a/src/constants/tokenConfig.ts +++ b/src/constants/tokenConfig.ts @@ -40,6 +40,7 @@ export interface OutputTokenDetails { offrampFeesBasisPoints: number; offrampFeesFixedComponent?: number; requiresClientMasterOverride: boolean; + usesMemo: boolean; } export const INPUT_TOKEN_CONFIG: Record = { usdc: { @@ -91,6 +92,7 @@ export const OUTPUT_TOKEN_CONFIG: Record = maxWithdrawalAmountRaw: '10000000000000000', offrampFeesBasisPoints: 125, requiresClientMasterOverride: false, + usesMemo: false, }, ars: { tomlFileUrl: 'https://api.anclap.com/.well-known/stellar.toml', @@ -116,6 +118,7 @@ export const OUTPUT_TOKEN_CONFIG: Record = offrampFeesBasisPoints: 200, // 2% offrampFeesFixedComponent: 10, // 10 ARS requiresClientMasterOverride: true, + usesMemo: true, }, }; diff --git a/src/hooks/useMainProcess.ts b/src/hooks/useMainProcess.ts index 417a6360..35437d71 100644 --- a/src/hooks/useMainProcess.ts +++ b/src/hooks/useMainProcess.ts @@ -6,7 +6,7 @@ import { INPUT_TOKEN_CONFIG, InputTokenType, OUTPUT_TOKEN_CONFIG, OutputTokenTyp import { fetchTomlValues, sep10, sep24Second } from '../services/anchor'; // Utils -import { useConfig, useSwitchChain } from 'wagmi'; +import { useAccount, useConfig, useSwitchChain } from 'wagmi'; import { polygon } from 'wagmi/chains'; import { OfframpingState, @@ -59,6 +59,7 @@ export const useMainProcess = () => { const [signingPhase, setSigningPhase] = useState(undefined); const wagmiConfig = useConfig(); + const { address } = useAccount(); const { switchChain } = useSwitchChain(); const { trackEvent, resetUniqueEvents } = useEventsContext(); @@ -141,6 +142,7 @@ export const useMainProcess = () => { tomlValues, stellarEphemeralSecret, outputTokenType, + address, addEvent, ); @@ -157,7 +159,7 @@ export const useMainProcess = () => { setAnchorSessionParams(anchorSessionParams); const fetchAndUpdateSep24Url = async () => { - const firstSep24Response = await sep24First(anchorSessionParams, sep10Account, outputTokenType); + const firstSep24Response = await sep24First(anchorSessionParams, sep10Account, outputTokenType, address); const url = new URL(firstSep24Response.url); url.searchParams.append('callback', 'postMessage'); firstSep24Response.url = url.toString(); @@ -185,7 +187,7 @@ export const useMainProcess = () => { } })(); }, - [offrampingStarted, offrampingState, switchChain, trackEvent], + [offrampingStarted, offrampingState, switchChain, trackEvent, address], ); const handleOnAnchorWindowOpen = useCallback(async () => { diff --git a/src/hooks/useSignChallenge.ts b/src/hooks/useSignChallenge.ts index 500b5391..bd04c2dd 100644 --- a/src/hooks/useSignChallenge.ts +++ b/src/hooks/useSignChallenge.ts @@ -1,5 +1,6 @@ import { useEffect, useState, useCallback } from 'react'; import { useSignMessage } from 'wagmi'; +import { SIGNING_SERVICE_URL } from '../constants/constants'; export function useSignChallenge(address: `0x${string}` | undefined) { const { signMessageAsync } = useSignMessage(); @@ -8,14 +9,19 @@ export function useSignChallenge(address: `0x${string}` | undefined) { const handleSiweSignIn = useCallback(async () => { try { - // const response = await fetch('/api/siwe-challenge'); - // const { message } = await response.json(); - const message = 'Please sign the message to log in'; - const signature = await signMessageAsync({ message }); + const response = await fetch(`${SIGNING_SERVICE_URL}/v1/siwe/create`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ walletAddress: address }), + }); + const { siweMessage, nonce } = await response.json(); + console.log('SIWE message:', siweMessage, 'nonce:', nonce); + const signature = await signMessageAsync({ message: siweMessage }); console.log('SIWE signature:', signature); - - localStorage.setItem(`siwe-signature-${address}`, signature); + localStorage.setItem(`siwe-signature-${address}`, JSON.stringify({ nonce, signature })); setIsModalOpen(false); } catch (error) { diff --git a/src/services/anchor/index.ts b/src/services/anchor/index.ts index cc8f036a..474e6fee 100644 --- a/src/services/anchor/index.ts +++ b/src/services/anchor/index.ts @@ -65,6 +65,8 @@ export const fetchTomlValues = async (TOML_FILE_URL: string): Promise { let sep10Account; if (requiresClientMasterOverride) { @@ -85,16 +87,31 @@ async function getUrlParams( sep10Account = ephemeralAccount; } + if (usesMemo) { + return { + urlParams: new URLSearchParams({ + account: sep10Account, + client_domain: config.applicationClientDomain, + memo: deriveMemoFromAddress(address), + }), + sep10Account, + }; + } return { urlParams: new URLSearchParams({ account: sep10Account, client_domain: config.applicationClientDomain }), sep10Account, }; } +const deriveMemoFromAddress = (address: `0x${string}`) => { + return address.slice(5, 15).replace(/\D/g, ''); +}; + export const sep10 = async ( tomlValues: TomlValues, stellarEphemeralSecret: string, outputToken: OutputTokenType, + address: `0x${string}` | undefined, renderEvent: (event: string, status: EventStatus) => void, ): Promise<{ sep10Account: string; token: string }> => { const { signingKey, webAuthEndpoint } = tomlValues; @@ -106,10 +123,10 @@ export const sep10 = async ( const ephemeralKeys = Keypair.fromSecret(stellarEphemeralSecret); const accountId = ephemeralKeys.publicKey(); - const { requiresClientMasterOverride } = OUTPUT_TOKEN_CONFIG[outputToken]; + const { requiresClientMasterOverride, usesMemo } = OUTPUT_TOKEN_CONFIG[outputToken]; // will select either clientMaster or the ephemeral account - const { urlParams, sep10Account } = await getUrlParams(accountId, requiresClientMasterOverride); + const { urlParams, sep10Account } = await getUrlParams(accountId, requiresClientMasterOverride, usesMemo, address!); const challenge = await fetch(`${webAuthEndpoint}?${urlParams.toString()}`); if (challenge.status !== 200) { @@ -129,16 +146,23 @@ export const sep10 = async ( throw new Error(`Invalid sequence number: ${transactionSigned.sequence}`); } - // More tests required, ignore for prototype + const maybeStoredSignatureString = localStorage.getItem(`siwe-signature-${address}`); + let nonce; + let signature; - // TODO fetch and check signing signature from localstore, - const maybeChallengeSignature = undefined; + // TODO actually, if usesMemo and not maybeStored.. we need to ask for it again. + if (maybeStoredSignatureString && usesMemo) { + const storedSignatureObject = JSON.parse(maybeStoredSignatureString); + nonce = storedSignatureObject.nonce; + signature = storedSignatureObject.signature; + } // sign both for client_domain + an extra signature for Anclap workaround const { masterSignature, clientSignature, clientPublic } = await fetchSep10Signatures( transactionSigned.toXDR(), outputToken, sep10Account, - maybeChallengeSignature, + signature, + nonce, ); transactionSigned.addSignature(clientPublic, clientSignature); @@ -233,6 +257,7 @@ export async function sep24First( sessionParams: IAnchorSessionParams, sep10Account: string, outputToken: OutputTokenType, + address: `0x${string}` | undefined, ): Promise { if (config.test.mockSep24) { return { url: 'https://www.example.com', id: '1234' }; @@ -253,6 +278,8 @@ export async function sep24First( amount: sessionParams.offrampAmount, account: sep10Account, // THIS is a particularity of Anclap. Should be able to work just with the epmhemeral account // Since we signed with the master from the service, we need to specify the corresponding public here + // memo: deriveMemoFromAddress(address!), + // memo_type: 'id', }); } else { sep24Params = new URLSearchParams({ diff --git a/src/services/signingService.tsx b/src/services/signingService.tsx index be2f94aa..b2606aa6 100644 --- a/src/services/signingService.tsx +++ b/src/services/signingService.tsx @@ -43,12 +43,13 @@ export const fetchSep10Signatures = async ( challengeXDR: string, outToken: OutputTokenType, clientPublicKey: string, - maybeChallengeSignature: any, + maybeChallengeSignature: string, + maybeNonce: string, ): Promise => { const response = await fetch(`${SIGNING_SERVICE_URL}/v1/stellar/sep10`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ challengeXDR, outToken, clientPublicKey, maybeChallengeSignature }), + body: JSON.stringify({ challengeXDR, outToken, clientPublicKey, maybeChallengeSignature, maybeNonce }), }); if (response.status !== 200) { throw new Error(`Failed to fetch SEP10 challenge from server: ${response.statusText}`); From 780b17b847dfe176a6ddc72a558cd30f0c518a7f Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Wed, 6 Nov 2024 17:05:33 -0300 Subject: [PATCH 006/221] remove comments --- signer-service/src/api/helpers/anchors.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/signer-service/src/api/helpers/anchors.js b/signer-service/src/api/helpers/anchors.js index 961d90cd..0c99e2f1 100644 --- a/signer-service/src/api/helpers/anchors.js +++ b/signer-service/src/api/helpers/anchors.js @@ -61,7 +61,6 @@ const verifyClientDomainChallengeOps = async ( throw new Error('First manageData operation should have a 64-byte random nonce as value'); } - // Flags to check presence of required operations let hasWebAuthDomain = false; let hasClientDomain = false; @@ -79,11 +78,6 @@ const verifyClientDomainChallengeOps = async ( if (op.source !== signingKey) { throw new Error('web_auth_domain manage_data operation must have the server account as the source'); } - - // value web_auth_domain but in bytes - // if (op.value !== 'web_auth_domain') { - // throw new Error(`web_auth_domain manageData operation should have value 'web_auth_domain'`); - // } } // Verify client_domain operation (if applicable) @@ -93,10 +87,6 @@ const verifyClientDomainChallengeOps = async ( if (op.source !== keypair.publicKey()) { throw new Error('client_domain manage_data operation must have the client domain account as the source'); } - // Also in bytes first - // if (op.value !== 'client_domain') { - // throw new Error(`client_domain manageData operation should have value 'client_domain'`); - // } } } From 1996e64467ca834f219d1806262e102258ad0581 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Wed, 6 Nov 2024 17:14:21 -0300 Subject: [PATCH 007/221] remove and improve comments --- signer-service/src/api/helpers/anchors.js | 82 +------------------ .../src/api/services/sep10.service.js | 9 +- .../src/api/services/siwe.service.js | 2 +- .../src/api/services/stellar.service.js | 2 +- signer-service/src/index.js | 42 ++++++---- 5 files changed, 34 insertions(+), 103 deletions(-) diff --git a/signer-service/src/api/helpers/anchors.js b/signer-service/src/api/helpers/anchors.js index 0c99e2f1..ba599ef9 100644 --- a/signer-service/src/api/helpers/anchors.js +++ b/signer-service/src/api/helpers/anchors.js @@ -19,84 +19,4 @@ const fetchTomlValues = async (tomlFileUrl) => { }; }; -const verifyClientDomainChallengeOps = async ( - challengeXDR, - networkPassphrase, - signingKey, - clientPublicKey, - memo, - expectedKey, -) => { - const transactionSigned = new TransactionBuilder.fromXDR(challengeXDR, networkPassphrase); - if (transactionSigned.source !== signingKey) { - throw new Error(`Invalid source account: ${transactionSigned.source}`); - } - if (transactionSigned.sequence !== '0') { - throw new Error(`Invalid sequence number: ${transactionSigned.sequence}`); - } - - // See https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md#success - // memo field should match and not be empty. - if (transactionSigned.memo.value !== memo) { - throw new Error('Memo does not match'); - } - - // Verify manage_data operations - const operations = transactionSigned.operations; - // Verify the first manage_data operation - const firstOp = operations[0]; - if (firstOp.type !== 'manageData') { - throw new Error('The first operation should be manageData'); - } - // We don't want to accept a challenge that would authorize as Application client account! - // We DO accept a challenge where the source is the master account + memo - if (firstOp.source !== clientPublicKey || firstOp.source == signingKey) { - throw new Error('First manageData operation must have the client account as the source'); - } - - if (firstOp.name !== expectedKey) { - throw new Error(`First manageData operation should have key '${expectedKey}'`); - } - if (!firstOp.value || firstOp.value.length !== 64) { - throw new Error('First manageData operation should have a 64-byte random nonce as value'); - } - - let hasWebAuthDomain = false; - let hasClientDomain = false; - - // Verify extra manage_data operations - for (let i = 1; i < operations.length; i++) { - const op = operations[i]; - - if (op.type !== 'manageData') { - throw new Error('All operations should be manage_data operations'); - } - - // Verify web_auth_domain operation - if (op.name === 'web_auth_domain') { - hasWebAuthDomain = true; - if (op.source !== signingKey) { - throw new Error('web_auth_domain manage_data operation must have the server account as the source'); - } - } - - // Verify client_domain operation (if applicable) - if (op.name === 'client_domain') { - hasClientDomain = true; - // Replace 'CLIENT_DOMAIN_ACCOUNT' with the actual client domain account public key - if (op.source !== keypair.publicKey()) { - throw new Error('client_domain manage_data operation must have the client domain account as the source'); - } - } - } - - // the web_auth_domain and client_domain operation must be present - if (!hasWebAuthDomain) { - throw new Error('Transaction must contain a web_auth_domain manageData operation'); - } - if (!hasClientDomain) { - throw new Error('Transaction must contain a client_domain manageData operation'); - } -}; - -module.exports = { fetchTomlValues, verifyClientDomainChallengeOps }; +module.exports = { fetchTomlValues }; diff --git a/signer-service/src/api/services/sep10.service.js b/signer-service/src/api/services/sep10.service.js index 8b00c50f..2e60c434 100644 --- a/signer-service/src/api/services/sep10.service.js +++ b/signer-service/src/api/services/sep10.service.js @@ -8,6 +8,8 @@ const { SEP10_MASTER_SECRET, CLIENT_SECRET } = require('../../constants/constant const NETWORK_PASSPHRASE = Networks.PUBLIC; +// we validate a challenge for a given nonce. From it we obtain the address and derive the memo +// we can then ensure that the memo is the same as the one we expect from the anchor challenge const getAndValidateMemo = async (nonce, userChallengeSignature) => { if (!userChallengeSignature || !nonce) { return ''; @@ -62,14 +64,15 @@ exports.signSep10Challenge = async (challengeXDR, outToken, clientPublicKey, use } // Temporary. This check will be removed when we have the memo solution. - if (outToken === 'ars') { + if (memo !== '') { // We only want to accept a challenge that would authorize the master key. if (firstOp.source !== masterStellarKeypair.publicKey()) { throw new Error('First manageData operation must have the master signing key as the source'); } } else { // Only authorize a session that corresponds with the ephemeral client account - if (firstOp.source !== clientPublicKey) { + // if no memo, we should not authorize a session for our client domain master + if (firstOp.source !== clientPublicKey || firstOp.source == masterStellarKeypair.publicKey()) { throw new Error('First manageData operation must have the client account as the source'); } } @@ -82,11 +85,9 @@ exports.signSep10Challenge = async (challengeXDR, outToken, clientPublicKey, use throw new Error('First manageData operation should have a 64-byte random nonce as value'); } - // Flags to check presence of required operations let hasWebAuthDomain = false; let hasClientDomain = false; - // Verify extra manage_data operations, web_auth and proper client domain. for (let i = 1; i < operations.length; i++) { const op = operations[i]; diff --git a/signer-service/src/api/services/siwe.service.js b/signer-service/src/api/services/siwe.service.js index 2e13995d..3f83af7a 100644 --- a/signer-service/src/api/services/siwe.service.js +++ b/signer-service/src/api/services/siwe.service.js @@ -6,7 +6,7 @@ const domain = 'localhost'; const origin = 'https://localhost/login'; const statement = 'Please sign the message to login!'; -// Set that will hold the siwe messages sent + nonce we defined +// Map that will hold the siwe messages sent + nonce we defined const siweMessagesMap = new Map(); exports.createAndSendSiweMessage = async (address) => { diff --git a/signer-service/src/api/services/stellar.service.js b/signer-service/src/api/services/stellar.service.js index 559ec2c5..f6d39372 100644 --- a/signer-service/src/api/services/stellar.service.js +++ b/signer-service/src/api/services/stellar.service.js @@ -7,7 +7,7 @@ const { STELLAR_EPHEMERAL_STARTING_BALANCE_UNITS, } = require('../../constants/constants'); const { TOKEN_CONFIG, getTokenConfigByAssetCode } = require('../../constants/tokenConfig'); -const { fetchTomlValues, verifyClientDomainChallengeOps } = require('../helpers/anchors'); + // Derive funding pk const FUNDING_PUBLIC_KEY = Keypair.fromSecret(FUNDING_SECRET).publicKey(); const horizonServer = new Horizon.Server(HORIZON_URL); diff --git a/signer-service/src/index.js b/signer-service/src/index.js index 1797a889..a3fe7ebd 100755 --- a/signer-service/src/index.js +++ b/signer-service/src/index.js @@ -4,25 +4,35 @@ const logger = require('./config/logger'); const app = require('./config/express'); require('dotenv').config(); -const { FUNDING_SECRET, PENDULUM_FUNDING_SEED, MOONBEAM_EXECUTOR_PRIVATE_KEY } = require('./constants/constants'); +const { + FUNDING_SECRET, + PENDULUM_FUNDING_SEED, + MOONBEAM_EXECUTOR_PRIVATE_KEY, + CLIENT_SECRET, +} = require('./constants/constants'); -// stop the application if the funding secret key is not set -// if (!FUNDING_SECRET) { -// logger.error('FUNDING_SECRET not set in the environment variables'); -// process.exit(1); -// } +//stop the application if the funding secret key is not set +if (!FUNDING_SECRET) { + logger.error('FUNDING_SECRET not set in the environment variables'); + process.exit(1); +} -// // stop the application if the Pendulum funding seed is not set -// if (!PENDULUM_FUNDING_SEED) { -// logger.error('PENDULUM_FUNDING_SEED not set in the environment variables'); -// process.exit(1); -// } +// stop the application if the Pendulum funding seed is not set +if (!PENDULUM_FUNDING_SEED) { + logger.error('PENDULUM_FUNDING_SEED not set in the environment variables'); + process.exit(1); +} -// // stop the application if the Moonbeam executor private key is not set -// if (!MOONBEAM_EXECUTOR_PRIVATE_KEY) { -// logger.error('MOONBEAM_EXECUTOR_PRIVATE_KEY not set in the environment variables'); -// process.exit(1); -// } +// stop the application if the Moonbeam executor private key is not set +if (!MOONBEAM_EXECUTOR_PRIVATE_KEY) { + logger.error('MOONBEAM_EXECUTOR_PRIVATE_KEY not set in the environment variables'); + process.exit(1); +} + +if (!CLIENT_SECRET) { + logger.error('CLIENT_SECRET not set in the environment variables'); + process.exit(1); +} // listen to requests app.listen(port, () => logger.info(`server started on port ${port} (${env})`)); From 96bd9b1ac8230339f6f4c09e454041559ea8bd65 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Fri, 8 Nov 2024 11:06:18 +0100 Subject: [PATCH 008/221] add usdt icon --- src/assets/coins/USDT.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/assets/coins/USDT.svg diff --git a/src/assets/coins/USDT.svg b/src/assets/coins/USDT.svg new file mode 100644 index 00000000..908769ba --- /dev/null +++ b/src/assets/coins/USDT.svg @@ -0,0 +1,3 @@ + + + From e9a4ce76124e188071929fe170b6f6e19175d7f5 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Fri, 8 Nov 2024 11:06:41 +0100 Subject: [PATCH 009/221] add usdt polygon icon --- src/assets/coins/USDT_POLYGON.svg | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/assets/coins/USDT_POLYGON.svg diff --git a/src/assets/coins/USDT_POLYGON.svg b/src/assets/coins/USDT_POLYGON.svg new file mode 100644 index 00000000..648dd2b4 --- /dev/null +++ b/src/assets/coins/USDT_POLYGON.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file From e4270cc72f585fd3cad99c76a276fb6919eb2821 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Fri, 8 Nov 2024 11:07:35 +0100 Subject: [PATCH 010/221] remove unused icons --- src/assets/coins/PEN.png | Bin 5582 -> 0 bytes src/assets/coins/USDT.png | Bin 147412 -> 0 bytes src/assets/coins/euro.svg | 12 ------------ 3 files changed, 12 deletions(-) delete mode 100644 src/assets/coins/PEN.png delete mode 100644 src/assets/coins/USDT.png delete mode 100644 src/assets/coins/euro.svg diff --git a/src/assets/coins/PEN.png b/src/assets/coins/PEN.png deleted file mode 100644 index 1ec53172e89173275aee2441d55facf851aac41c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5582 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Ν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 diff --git a/src/assets/coins/euro.svg b/src/assets/coins/euro.svg deleted file mode 100644 index 89d9381d..00000000 --- a/src/assets/coins/euro.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - \ No newline at end of file From b4eaddf5bc8fa5965db85ed4584e55168a71896d Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Fri, 8 Nov 2024 17:40:52 -0300 Subject: [PATCH 011/221] remove master account, either use memo or not --- src/constants/tokenConfig.ts | 3 --- src/services/anchor/index.ts | 36 +++++++++++++++--------------------- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/src/constants/tokenConfig.ts b/src/constants/tokenConfig.ts index 6884b904..7db36768 100644 --- a/src/constants/tokenConfig.ts +++ b/src/constants/tokenConfig.ts @@ -39,7 +39,6 @@ export interface OutputTokenDetails { erc20WrapperAddress: string; offrampFeesBasisPoints: number; offrampFeesFixedComponent?: number; - requiresClientMasterOverride: boolean; usesMemo: boolean; } export const INPUT_TOKEN_CONFIG: Record = { @@ -91,7 +90,6 @@ export const OUTPUT_TOKEN_CONFIG: Record = minWithdrawalAmountRaw: '10000000000000', maxWithdrawalAmountRaw: '10000000000000000', offrampFeesBasisPoints: 125, - requiresClientMasterOverride: false, usesMemo: false, }, ars: { @@ -117,7 +115,6 @@ export const OUTPUT_TOKEN_CONFIG: Record = maxWithdrawalAmountRaw: '500000000000000000', // 500000 ARS offrampFeesBasisPoints: 200, // 2% offrampFeesFixedComponent: 10, // 10 ARS - requiresClientMasterOverride: true, usesMemo: true, }, }; diff --git a/src/services/anchor/index.ts b/src/services/anchor/index.ts index 474e6fee..5480fde5 100644 --- a/src/services/anchor/index.ts +++ b/src/services/anchor/index.ts @@ -62,32 +62,25 @@ export const fetchTomlValues = async (TOML_FILE_URL: string): Promise { - let sep10Account; - if (requiresClientMasterOverride) { + if (usesMemo) { const response = await fetch(`${SIGNING_SERVICE_URL}/v1/stellar/sep10`); if (!response.ok) { throw new Error('Failed to fetch client master SEP-10 public account.'); } - const { masterSep10Public } = await response.json(); - if (!masterSep10Public) { throw new Error('masterSep10Public not found in response.'); } - sep10Account = masterSep10Public; - } else { - sep10Account = ephemeralAccount; - } + const sep10Account = masterSep10Public; - if (usesMemo) { return { urlParams: new URLSearchParams({ account: sep10Account, @@ -98,11 +91,12 @@ async function getUrlParams( }; } return { - urlParams: new URLSearchParams({ account: sep10Account, client_domain: config.applicationClientDomain }), - sep10Account, + urlParams: new URLSearchParams({ account: ephemeralAccount, client_domain: config.applicationClientDomain }), + sep10Account: ephemeralAccount, }; } +//TODO A very naive memo derivation for testing. NOT SECURE const deriveMemoFromAddress = (address: `0x${string}`) => { return address.slice(5, 15).replace(/\D/g, ''); }; @@ -123,10 +117,10 @@ export const sep10 = async ( const ephemeralKeys = Keypair.fromSecret(stellarEphemeralSecret); const accountId = ephemeralKeys.publicKey(); - const { requiresClientMasterOverride, usesMemo } = OUTPUT_TOKEN_CONFIG[outputToken]; + const { usesMemo } = OUTPUT_TOKEN_CONFIG[outputToken]; // will select either clientMaster or the ephemeral account - const { urlParams, sep10Account } = await getUrlParams(accountId, requiresClientMasterOverride, usesMemo, address!); + const { urlParams, sep10Account } = await getUrlParams(accountId, usesMemo, address!); const challenge = await fetch(`${webAuthEndpoint}?${urlParams.toString()}`); if (challenge.status !== 200) { @@ -146,6 +140,8 @@ export const sep10 = async ( throw new Error(`Invalid sequence number: ${transactionSigned.sequence}`); } + // TODO change to add a fx that will either try to get the signature from storage, + // check if it's still valid, and if not ask for another one. const maybeStoredSignatureString = localStorage.getItem(`siwe-signature-${address}`); let nonce; let signature; @@ -166,7 +162,7 @@ export const sep10 = async ( ); transactionSigned.addSignature(clientPublic, clientSignature); - if (!requiresClientMasterOverride) { + if (!usesMemo) { transactionSigned.sign(ephemeralKeys); } else { transactionSigned.addSignature(sep10Account, masterSignature); @@ -266,18 +262,16 @@ export async function sep24First( const { token, tomlValues } = sessionParams; const { sep24Url } = tomlValues; - const { requiresClientMasterOverride } = OUTPUT_TOKEN_CONFIG[outputToken]; + const { usesMemo } = OUTPUT_TOKEN_CONFIG[outputToken]; let sep24Params; - if (requiresClientMasterOverride) { - if (!sep10Account) { - throw new Error('Master must be defined at this point.'); - } + if (usesMemo) { sep24Params = new URLSearchParams({ asset_code: sessionParams.tokenConfig.stellarAsset.code.string.replace('\0', ''), amount: sessionParams.offrampAmount, account: sep10Account, // THIS is a particularity of Anclap. Should be able to work just with the epmhemeral account - // Since we signed with the master from the service, we need to specify the corresponding public here + // or at least the anchor should be able to get it from the JWT. + // Since we signed with the master/omnibus from the service, we need to specify the corresponding public here // memo: deriveMemoFromAddress(address!), // memo_type: 'id', }); From 79beb7752c983003b56177c68a4e26692b3a4e0c Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Fri, 8 Nov 2024 19:22:53 -0300 Subject: [PATCH 012/221] improve signature hook --- package.json | 2 + src/constants/localStorage.ts | 1 + src/hooks/useMainProcess.ts | 4 +- src/hooks/useSignChallenge.ts | 74 +++++++++++++++++++----- src/services/anchor/index.ts | 8 ++- yarn.lock | 105 +++++++++++++++++++++++++++++++--- 6 files changed, 168 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 47283bfa..10f1565b 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "bn.js": "^5.2.1", "buffer": "^6.0.3", "daisyui": "^4.11.1", + "ethers": "^6.13.4", "framer-motion": "^11.2.14", "postcss": "^8.4.38", "preact": "^10.12.1", @@ -56,6 +57,7 @@ "react-hook-form": "^7.51.5", "react-router-dom": "^6.8.1", "react-toastify": "^10.0.5", + "siwe": "^2.3.2", "stellar-base": "^11.0.1", "stellar-sdk": "^11.3.0", "tailwind": "^4.0.0", diff --git a/src/constants/localStorage.ts b/src/constants/localStorage.ts index 6c22744b..df293d32 100644 --- a/src/constants/localStorage.ts +++ b/src/constants/localStorage.ts @@ -10,6 +10,7 @@ export const storageKeys = { ANCHOR_SESSION_PARAMS: 'ANCHOR_SESSION_PARAMS', STELLAR_OPERATIONS: 'STELLAR_OPERATIONS', TOKEN_BRIDGED_AMOUNT: 'TOKEN_BRIDGED_AMOUNT', + SIWE_SIGNATURE_KEY_PREFIX: 'siwe-signature-', // Internal squidrouter recovery states SQUIDROUTER_RECOVERY_STATE: 'SQUIDROUTER_TRANSACTION_STATE', diff --git a/src/hooks/useMainProcess.ts b/src/hooks/useMainProcess.ts index 35437d71..e2492186 100644 --- a/src/hooks/useMainProcess.ts +++ b/src/hooks/useMainProcess.ts @@ -22,7 +22,7 @@ import { createTransactionEvent, useEventsContext } from '../contexts/events'; import { showToast, ToastMessage } from '../helpers/notifications'; import { IAnchorSessionParams, ISep24Intermediate } from '../services/anchor'; import { OFFRAMPING_PHASE_SECONDS } from '../pages/progress'; - +import { useGetOrRefreshSiweSignature } from './useSignChallenge'; export type SigningPhase = 'started' | 'approved' | 'signed' | 'finished'; export interface ExecutionInput { @@ -64,6 +64,7 @@ export const useMainProcess = () => { const { trackEvent, resetUniqueEvents } = useEventsContext(); const [, setEvents] = useState([]); + const { getOrRefreshSiweSignature } = useGetOrRefreshSiweSignature(address); const updateHookStateFromState = useCallback( (state: OfframpingState | undefined) => { @@ -143,6 +144,7 @@ export const useMainProcess = () => { stellarEphemeralSecret, outputTokenType, address, + getOrRefreshSiweSignature, addEvent, ); diff --git a/src/hooks/useSignChallenge.ts b/src/hooks/useSignChallenge.ts index bd04c2dd..8fbd060b 100644 --- a/src/hooks/useSignChallenge.ts +++ b/src/hooks/useSignChallenge.ts @@ -1,13 +1,42 @@ import { useEffect, useState, useCallback } from 'react'; import { useSignMessage } from 'wagmi'; import { SIGNING_SERVICE_URL } from '../constants/constants'; +import { storageKeys } from '../constants/localStorage'; +import { SiweMessage } from 'siwe'; -export function useSignChallenge(address: `0x${string}` | undefined) { +type SiweSignatureData = { + nonce: string; + signature: string; + expirationDate: string; +}; + +export function useGetOrRefreshSiweSignature(address: `0x${string}` | undefined) { const { signMessageAsync } = useSignMessage(); + const [signatureData, setSignatureData] = useState(null); - const [isModalOpen, setIsModalOpen] = useState(false); + const getOrRefreshSiweSignature = useCallback(async (): Promise => { + if (!address) { + return; + } - const handleSiweSignIn = useCallback(async () => { + const storageKey = `${storageKeys.SIWE_SIGNATURE_KEY_PREFIX}${address}`; + const maybeStoredSignatureData = localStorage.getItem(storageKey); + + if (maybeStoredSignatureData) { + const storedSignatureData: SiweSignatureData = JSON.parse(maybeStoredSignatureData); + const expirationDate = new Date(storedSignatureData.expirationDate); + + if (expirationDate > new Date()) { + // Signature is still valid + setSignatureData(storedSignatureData); + return storedSignatureData; + } else { + // Signature expired, remove it + localStorage.removeItem(storageKey); + } + } + + // Signature not found or expired, fetch a new one try { const response = await fetch(`${SIGNING_SERVICE_URL}/v1/siwe/create`, { method: 'POST', @@ -16,37 +45,56 @@ export function useSignChallenge(address: `0x${string}` | undefined) { }, body: JSON.stringify({ walletAddress: address }), }); + const { siweMessage, nonce } = await response.json(); - console.log('SIWE message:', siweMessage, 'nonce:', nonce); + + // Parse the SIWE message to extract the expiration date + const message = new SiweMessage(siweMessage); + const expirationDate = message.expirationTime!; + const signature = await signMessageAsync({ message: siweMessage }); - console.log('SIWE signature:', signature); - localStorage.setItem(`siwe-signature-${address}`, JSON.stringify({ nonce, signature })); + const newSignatureData: SiweSignatureData = { + nonce, + signature, + expirationDate, + }; - setIsModalOpen(false); + localStorage.setItem(storageKey, JSON.stringify(newSignatureData)); + setSignatureData(newSignatureData); + return newSignatureData; } catch (error) { console.error('Error during SIWE sign-in:', error); } }, [address, signMessageAsync]); + return { signatureData, getOrRefreshSiweSignature }; +} + +export function useSignChallenge(address: `0x${string}` | undefined) { + const { signatureData, getOrRefreshSiweSignature } = useGetOrRefreshSiweSignature(address); + const [isModalOpen, setIsModalOpen] = useState(false); + + useEffect(() => { + getOrRefreshSiweSignature(); + }, [address]); + useEffect(() => { if (!address) { + setIsModalOpen(false); return; } - const storedSignature = localStorage.getItem(`siwe-signature-${address}`); - console.log('Stored SIWE signature:', storedSignature); - if (!storedSignature) { + if (!signatureData) { setIsModalOpen(true); - console.log('Opening SIWE sign-in modal'); } else { setIsModalOpen(false); } - }, [address]); + }, [address, signatureData]); return { isModalOpen, - handleSiweSignIn, + handleSiweSignIn: getOrRefreshSiweSignature, closeModal: () => setIsModalOpen(false), }; } diff --git a/src/services/anchor/index.ts b/src/services/anchor/index.ts index 5480fde5..423badc3 100644 --- a/src/services/anchor/index.ts +++ b/src/services/anchor/index.ts @@ -106,6 +106,7 @@ export const sep10 = async ( stellarEphemeralSecret: string, outputToken: OutputTokenType, address: `0x${string}` | undefined, + getOrRefreshSiweSignature: any, renderEvent: (event: string, status: EventStatus) => void, ): Promise<{ sep10Account: string; token: string }> => { const { signingKey, webAuthEndpoint } = tomlValues; @@ -142,13 +143,14 @@ export const sep10 = async ( // TODO change to add a fx that will either try to get the signature from storage, // check if it's still valid, and if not ask for another one. - const maybeStoredSignatureString = localStorage.getItem(`siwe-signature-${address}`); + const signatureData = await getOrRefreshSiweSignature(); + console.log('fetched: ', signatureData); let nonce; let signature; // TODO actually, if usesMemo and not maybeStored.. we need to ask for it again. - if (maybeStoredSignatureString && usesMemo) { - const storedSignatureObject = JSON.parse(maybeStoredSignatureString); + if (signatureData && usesMemo) { + const storedSignatureObject = signatureData.signature; nonce = storedSignatureObject.nonce; signature = storedSignatureObject.signature; } diff --git a/yarn.lock b/yarn.lock index 98bf645b..cea380f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,7 +12,7 @@ __metadata: languageName: node linkType: hard -"@adraffy/ens-normalize@npm:^1.8.8": +"@adraffy/ens-normalize@npm:1.10.1, @adraffy/ens-normalize@npm:^1.8.8": version: 1.10.1 resolution: "@adraffy/ens-normalize@npm:1.10.1" checksum: 10/4cb938c4abb88a346d50cb0ea44243ab3574330c81d4f5aaaf9dfee584b96189d0faa404de0fcbef5a1b73909ea4ebc3e63d84bd23f9949e5c8d4085207a5091 @@ -2621,6 +2621,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:^1.1.2": + version: 1.5.0 + resolution: "@noble/hashes@npm:1.5.0" + checksum: 10/da7fc7af52af7afcf59810a7eea6155075464ff462ffda2572dc6d57d53e2669b1ea2ec774e814f6273f1697e567f28d36823776c9bf7068cba2a2855140f26e + languageName: node + linkType: hard + "@noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.2": version: 1.3.3 resolution: "@noble/hashes@npm:1.3.3" @@ -4455,6 +4462,18 @@ __metadata: languageName: node linkType: hard +"@spruceid/siwe-parser@npm:^2.1.2": + version: 2.1.2 + resolution: "@spruceid/siwe-parser@npm:2.1.2" + dependencies: + "@noble/hashes": "npm:^1.1.2" + apg-js: "npm:^4.3.0" + uri-js: "npm:^4.4.1" + valid-url: "npm:^1.0.9" + checksum: 10/48459fe3b4d4b3091375ee87af700864c9023d4a1271d34850c6d27475e5d93a45d1efe8a71da367ad838b6921ced60c387d54737edd0a7a0d8e4e0a3cc2b8b7 + languageName: node + linkType: hard + "@stablelib/aead@npm:^1.0.1": version: 1.0.1 resolution: "@stablelib/aead@npm:1.0.1" @@ -5013,6 +5032,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:22.7.5": + version: 22.7.5 + resolution: "@types/node@npm:22.7.5" + dependencies: + undici-types: "npm:~6.19.2" + checksum: 10/e8ba102f8c1aa7623787d625389be68d64e54fcbb76d41f6c2c64e8cf4c9f4a2370e7ef5e5f1732f3c57529d3d26afdcb2edc0101c5e413a79081449825c57ac + languageName: node + linkType: hard + "@types/parse-json@npm:^4.0.0": version: 4.0.0 resolution: "@types/parse-json@npm:4.0.0" @@ -5953,6 +5981,13 @@ __metadata: languageName: node linkType: hard +"aes-js@npm:4.0.0-beta.5": + version: 4.0.0-beta.5 + resolution: "aes-js@npm:4.0.0-beta.5" + checksum: 10/8f745da2e8fb38e91297a8ec13c2febe3219f8383303cd4ed4660ca67190242ccfd5fdc2f0d1642fd1ea934818fb871cd4cc28d3f28e812e3dc6c3d0f1f97c24 + languageName: node + linkType: hard + "agent-base@npm:6, agent-base@npm:^6.0.2": version: 6.0.2 resolution: "agent-base@npm:6.0.2" @@ -6090,6 +6125,13 @@ __metadata: languageName: node linkType: hard +"apg-js@npm:^4.3.0": + version: 4.4.0 + resolution: "apg-js@npm:4.4.0" + checksum: 10/425f19096026742f5f156f26542b68f55602aa60f0c4ae2d72a0a888cf15fe9622223191202262dd8979d76a6125de9d8fd164d56c95fb113f49099f405eb08c + languageName: node + linkType: hard + "app-root-path@npm:2.1.0": version: 2.1.0 resolution: "app-root-path@npm:2.1.0" @@ -9060,6 +9102,21 @@ __metadata: languageName: node linkType: hard +"ethers@npm:^6.13.4": + version: 6.13.4 + resolution: "ethers@npm:6.13.4" + dependencies: + "@adraffy/ens-normalize": "npm:1.10.1" + "@noble/curves": "npm:1.2.0" + "@noble/hashes": "npm:1.3.2" + "@types/node": "npm:22.7.5" + aes-js: "npm:4.0.0-beta.5" + tslib: "npm:2.7.0" + ws: "npm:8.17.1" + checksum: 10/221192fed93f6b0553f3e5e72bfd667d676220577d34ff854f677e955d6f608e60636a9c08b5d54039c532a9b9b7056384f0d7019eb6e111d53175806f896ac6 + languageName: node + linkType: hard + "event-emitter@npm:^0.3.5": version: 0.3.5 resolution: "event-emitter@npm:0.3.5" @@ -14821,6 +14878,20 @@ __metadata: languageName: node linkType: hard +"siwe@npm:^2.3.2": + version: 2.3.2 + resolution: "siwe@npm:2.3.2" + dependencies: + "@spruceid/siwe-parser": "npm:^2.1.2" + "@stablelib/random": "npm:^1.0.1" + uri-js: "npm:^4.4.1" + valid-url: "npm:^1.0.9" + peerDependencies: + ethers: ^5.6.8 || ^6.0.8 + checksum: 10/6ea5ad9a9046fa916f85bf9d3092bc898f7e339d9c552714ea53ecc17daa4f78300c3cf7cc9c70fe57baf77dcee5cb38c6e1d692400b874cd84d297b1261918c + languageName: node + linkType: hard + "slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -15672,6 +15743,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:2.7.0, tslib@npm:^2.7.0": + version: 2.7.0 + resolution: "tslib@npm:2.7.0" + checksum: 10/9a5b47ddac65874fa011c20ff76db69f97cf90c78cff5934799ab8894a5342db2d17b4e7613a087046bc1d133d21547ddff87ac558abeec31ffa929c88b7fce6 + languageName: node + linkType: hard + "tslib@npm:^2.0.0, tslib@npm:^2.4.0": version: 2.6.3 resolution: "tslib@npm:2.6.3" @@ -15693,13 +15771,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.7.0": - version: 2.7.0 - resolution: "tslib@npm:2.7.0" - checksum: 10/9a5b47ddac65874fa011c20ff76db69f97cf90c78cff5934799ab8894a5342db2d17b4e7613a087046bc1d133d21547ddff87ac558abeec31ffa929c88b7fce6 - languageName: node - linkType: hard - "tsscmp@npm:^1.0.5": version: 1.0.6 resolution: "tsscmp@npm:1.0.6" @@ -15942,6 +16013,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.19.2": + version: 6.19.8 + resolution: "undici-types@npm:6.19.8" + checksum: 10/cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70 + languageName: node + linkType: hard + "unenv@npm:^1.9.0": version: 1.9.0 resolution: "unenv@npm:1.9.0" @@ -16179,7 +16257,7 @@ __metadata: languageName: node linkType: hard -"uri-js@npm:^4.2.2": +"uri-js@npm:^4.2.2, uri-js@npm:^4.4.1": version: 4.4.1 resolution: "uri-js@npm:4.4.1" dependencies: @@ -16355,6 +16433,13 @@ __metadata: languageName: node linkType: hard +"valid-url@npm:^1.0.9": + version: 1.0.9 + resolution: "valid-url@npm:1.0.9" + checksum: 10/343dfaf85eb3691dc8eb93f7bc007be1ee6091e6c6d1a68bf633cb85e4bf2930e34ca9214fb2c3330de5b652510b257a8ee1ff0a0a37df0925e9dabf93ee512d + languageName: node + linkType: hard + "valtio@npm:1.11.2": version: 1.11.2 resolution: "valtio@npm:1.11.2" @@ -16606,6 +16691,7 @@ __metadata: eslint: "npm:^8.34.0" eslint-plugin-react: "npm:^7.32.2" eslint-plugin-react-hooks: "npm:^4.6.0" + ethers: "npm:^6.13.4" framer-motion: "npm:^11.2.14" happy-dom: "npm:^14.12.3" husky: "npm:>=6" @@ -16619,6 +16705,7 @@ __metadata: react-hook-form: "npm:^7.51.5" react-router-dom: "npm:^6.8.1" react-toastify: "npm:^10.0.5" + siwe: "npm:^2.3.2" stellar-base: "npm:^11.0.1" stellar-sdk: "npm:^11.3.0" tailwind: "npm:^4.0.0" From ec8e798931afd1656facfac31c67281700cfe941 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Mon, 11 Nov 2024 11:55:33 -0300 Subject: [PATCH 013/221] use viem to validate message on backend, error handling --- .../src/api/services/sep10.service.js | 16 ++++++--- .../src/api/services/siwe.service.js | 34 +++++++++++++------ src/hooks/useSignChallenge.ts | 2 +- src/services/anchor/index.ts | 12 +++---- src/services/signingService.tsx | 4 +-- 5 files changed, 43 insertions(+), 25 deletions(-) diff --git a/signer-service/src/api/services/sep10.service.js b/signer-service/src/api/services/sep10.service.js index af2feff0..540c3f3f 100644 --- a/signer-service/src/api/services/sep10.service.js +++ b/signer-service/src/api/services/sep10.service.js @@ -14,9 +14,15 @@ const getAndValidateMemo = async (nonce, userChallengeSignature) => { if (!userChallengeSignature || !nonce) { return null; // Default memo value when single stellar account is used } - const siweData = await verifySiweMessage(nonce, userChallengeSignature); - const memo = deriveMemoFromAddress(siweData.address); + let message; + try { + message = await verifySiweMessage(nonce, userChallengeSignature); + } catch (e) { + throw new Error(`Could not verify signature: ${e.message}`); + } + + const memo = deriveMemoFromAddress(message.address); return memo; }; @@ -33,10 +39,10 @@ exports.signSep10Challenge = async (challengeXDR, outToken, clientPublicKey, use let memo; try { - memo = getAndValidateMemo(nonce, userChallengeSignature); + memo = await getAndValidateMemo(nonce, userChallengeSignature); } catch (e) { console.log(e); - throw new Error(`Invalid evm account verification`); + throw new Error(`Could not verify signature or derive memo: ${e.message}`); } const { signingKey: anchorSigningKey } = await fetchTomlValues(TOKEN_CONFIG[outToken].tomlFileUrl); @@ -53,7 +59,7 @@ exports.signSep10Challenge = async (challengeXDR, outToken, clientPublicKey, use // See https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md#success // memo field should be empty as we assume for the ephemeral case, or the corresponding evm address // derivation. - if (transactionSigned.memo.value === memo) { + if (transactionSigned.memo.value !== memo) { throw new Error('Memo does not match with specified user signature or address. Could not validate.'); } diff --git a/signer-service/src/api/services/siwe.service.js b/signer-service/src/api/services/siwe.service.js index 3f83af7a..94622006 100644 --- a/signer-service/src/api/services/siwe.service.js +++ b/signer-service/src/api/services/siwe.service.js @@ -1,4 +1,6 @@ const siwe = require('siwe'); +const { createPublicClient, http } = require('viem'); +const { polygon } = require('viem/chains'); // Make constants on config const scheme = 'https'; @@ -22,33 +24,43 @@ exports.createAndSendSiweMessage = async (address) => { nonce, expirationTime: new Date(Date.now() + 360 * 60 * 1000).toISOString(), }); - const preparedMessage = siweMessage.prepareMessage(); - siweMessagesMap.set(nonce, siweMessage); + const preparedMessage = siweMessage.toMessage(); + siweMessagesMap.set(nonce, { siweMessage, address }); return { siweMessage: preparedMessage, nonce }; }; exports.verifySiweMessage = async (nonce, signature) => { - const maybeSiweMessage = siweMessagesMap.get(nonce); - if (!maybeSiweMessage) { + const maybeSiweData = siweMessagesMap.get(nonce); + if (!maybeSiweData) { throw new Error('Message not found, we have not send this message or nonce is incorrect.'); } - // TODO DEFINE at some point we need to delete them (?) - //siweMessagesMap.delete(nonce); - // Verify the signature and other message fields - const { data } = await maybeSiweMessage.verify({ signature }); + const publicClient = createPublicClient({ + chain: polygon, + transport: http(), + }); + + const valid = await publicClient.verifyMessage({ + address: maybeSiweData.address, + message: maybeSiweData.siweMessage.toMessage(), + signature, + }); + + if (!valid) { + throw new Error('Invalid signature.'); + } // Perform additional checks to ensure message integrity - if (data.nonce !== nonce) { + if (maybeSiweData.siweMessage.nonce !== nonce) { throw new Error('Nonce mismatch.'); } - if (data.expirationTime && new Date(data.expirationTime) < new Date()) { + if (maybeSiweData.expirationTime && new Date(maybeSiweData.expirationTime) < new Date()) { throw new Error('Message has expired.'); } - return data; + return maybeSiweData.siweMessage; }; // TODO we need some sort of session log-out. diff --git a/src/hooks/useSignChallenge.ts b/src/hooks/useSignChallenge.ts index 8fbd060b..5625cf9e 100644 --- a/src/hooks/useSignChallenge.ts +++ b/src/hooks/useSignChallenge.ts @@ -4,7 +4,7 @@ import { SIGNING_SERVICE_URL } from '../constants/constants'; import { storageKeys } from '../constants/localStorage'; import { SiweMessage } from 'siwe'; -type SiweSignatureData = { +export type SiweSignatureData = { nonce: string; signature: string; expirationDate: string; diff --git a/src/services/anchor/index.ts b/src/services/anchor/index.ts index 7c8423d5..667630b9 100644 --- a/src/services/anchor/index.ts +++ b/src/services/anchor/index.ts @@ -2,6 +2,7 @@ import { Transaction, Keypair, Networks } from 'stellar-sdk'; import { EventStatus } from '../../components/GenericEvent'; import { OutputTokenDetails, OutputTokenType } from '../../constants/tokenConfig'; import { fetchSep10Signatures, fetchSigningServiceAccountId } from '../signingService'; +import { SiweSignatureData } from '../../hooks/useSignChallenge'; import { config } from '../../config'; import { OUTPUT_TOKEN_CONFIG } from '../../constants/tokenConfig'; @@ -144,15 +145,14 @@ export const sep10 = async ( // TODO change to add a fx that will either try to get the signature from storage, // check if it's still valid, and if not ask for another one. - const signatureData = await getOrRefreshSiweSignature(); - console.log('fetched: ', signatureData); + const signatureData: SiweSignatureData = await getOrRefreshSiweSignature(); + + // undefined if not using memo let nonce; let signature; - - // TODO actually, if usesMemo and not maybeStored.. we need to ask for it again. if (signatureData && usesMemo) { - nonce = signatureData.signature.nonce; - signature = signatureData.signature.signature; + nonce = signatureData.nonce; + signature = signatureData.signature; } // sign both for client_domain + an extra signature for Anclap workaround const { masterClientSignature, clientSignature, clientPublic } = await fetchSep10Signatures( diff --git a/src/services/signingService.tsx b/src/services/signingService.tsx index 557e78c6..0099ba36 100644 --- a/src/services/signingService.tsx +++ b/src/services/signingService.tsx @@ -43,8 +43,8 @@ export const fetchSep10Signatures = async ( challengeXDR: string, outToken: OutputTokenType, clientPublicKey: string, - maybeChallengeSignature: string, - maybeNonce: string, + maybeChallengeSignature: string | undefined, + maybeNonce: string | undefined, ): Promise => { const response = await fetch(`${SIGNING_SERVICE_URL}/v1/stellar/sep10`, { method: 'POST', From 2b14bbebe708ad72a35daa313f0a0eb84d6f79c3 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Mon, 11 Nov 2024 13:55:40 -0300 Subject: [PATCH 014/221] improve sign in hooks --- .../src/api/controllers/stellar.controller.js | 13 ++++ src/components/SignIn/index.tsx | 21 +++--- src/hooks/useMainProcess.ts | 13 ++-- src/hooks/useSignChallenge.ts | 71 ++++++++++--------- src/pages/swap/index.tsx | 7 +- src/services/anchor/index.ts | 18 +++-- src/services/signingService.tsx | 3 + 7 files changed, 90 insertions(+), 56 deletions(-) diff --git a/signer-service/src/api/controllers/stellar.controller.js b/signer-service/src/api/controllers/stellar.controller.js index 6cad8ee7..ea7b7e91 100644 --- a/signer-service/src/api/controllers/stellar.controller.js +++ b/signer-service/src/api/controllers/stellar.controller.js @@ -63,6 +63,19 @@ exports.signSep10Challenge = async (req, res, next) => { ); return res.json({ masterClientSignature, masterClientPublic, clientSignature, clientPublic }); } catch (error) { + if (error.message.includes('Could not verify signature')) { + // Distinguish between failed signature check and other errors. + try { + return res.status(401).json({ + error: 'Signature validation failed.', + details: error.message, + }); + } catch (error) { + console.error('Error in signSep10Challenge:', error); + return res.status(500).json({ error: 'Failed to sign challenge', details: error.message }); + } + } + console.error('Error in signSep10Challenge:', error); return res.status(500).json({ error: 'Failed to sign challenge', details: error.message }); } diff --git a/src/components/SignIn/index.tsx b/src/components/SignIn/index.tsx index f61d6e6c..68726b94 100644 --- a/src/components/SignIn/index.tsx +++ b/src/components/SignIn/index.tsx @@ -1,20 +1,23 @@ -import React from 'react'; +import { FC } from 'react'; import { useAccount } from 'wagmi'; -import { useSignChallenge } from '../../hooks/useSignChallenge'; import { Modal } from 'react-daisyui'; -export function SignInModal() { +interface SignInModalProps { + requiresSign: boolean; + closeModal: any; + handleSignIn: any; +} + +export const SignInModal: FC = ({ requiresSign, closeModal, handleSignIn }) => { const { address } = useAccount(); console.log('address:', address); - const { isModalOpen, handleSiweSignIn, closeModal } = useSignChallenge(address); - - if (!isModalOpen) { + if (!requiresSign) { return null; } return ( - + Sign In - ); - } + const { address, isConnected, caipAddress, status } = useAppKitAccount(); - if (chain.unsupported) { - return ( - - ); - } + const ready = status === 'connected'; - return ( - <> - - - ); - })()} -
- ); - }} - + return ( +
+ +
); } diff --git a/src/components/buttons/SwapSubmitButton/index.tsx b/src/components/buttons/SwapSubmitButton/index.tsx index 27753119..e3bf149e 100644 --- a/src/components/buttons/SwapSubmitButton/index.tsx +++ b/src/components/buttons/SwapSubmitButton/index.tsx @@ -1,45 +1,38 @@ -import { ConnectButton } from '@rainbow-me/rainbowkit'; import { FC } from 'preact/compat'; import { Spinner } from '../../Spinner'; +import { useAppKit, useAppKitAccount } from '@reown/appkit/react'; interface SwapSubmitButtonProps { text: string; disabled: boolean; pending: boolean; } -export const SwapSubmitButton: FC = ({ text, disabled, pending }) => ( - - {({ account, chain, openConnectModal, authenticationStatus, mounted }) => { - const ready = mounted && authenticationStatus !== 'loading'; - const connected = - ready && account && chain && (!authenticationStatus || authenticationStatus === 'authenticated'); - const showInDisabledState = disabled || pending; +export const SwapSubmitButton: FC = ({ text, disabled, pending }) => { + const showInDisabledState = disabled || pending; - return ( -
- {(() => { - if (!connected) { - return ( - - ); - } + const { open, close } = useAppKit(); - return ( - - ); - })()} -
- ); - }} -
-); + const { address, isConnected, caipAddress, status } = useAppKitAccount(); + + return ( +
+ {(() => { + if (!isConnected) { + return ( + + ); + } + + return ( + + ); + })()} +
+ ); +}; diff --git a/src/constants/tokenConfig.ts b/src/constants/tokenConfig.ts index 0adf57fa..5b856a0b 100644 --- a/src/constants/tokenConfig.ts +++ b/src/constants/tokenConfig.ts @@ -39,6 +39,7 @@ export interface OutputTokenDetails { erc20WrapperAddress: string; offrampFeesBasisPoints: number; } + export const INPUT_TOKEN_CONFIG: Record = { usdc: { assetSymbol: 'USDC', @@ -64,7 +65,7 @@ export const INPUT_TOKEN_CONFIG: Record = { }, }; -export type OutputTokenType = 'eurc'; +export type OutputTokenType = 'eurc' | 'ars'; export const OUTPUT_TOKEN_CONFIG: Record = { eurc: { tomlFileUrl: 'https://circle.anchor.mykobo.co/.well-known/stellar.toml', @@ -89,6 +90,29 @@ export const OUTPUT_TOKEN_CONFIG: Record = maxWithdrawalAmountRaw: '10000000000000000', offrampFeesBasisPoints: 125, }, + ars: { + tomlFileUrl: 'https://api.anclap.com/.well-known/stellar.toml', + decimals: 12, + fiat: { + assetIcon: 'eur', + symbol: 'ARS', + }, + stellarAsset: { + code: { + hex: '0x41525300', + string: 'ARS\0', + }, + issuer: { + hex: '0xb04f8bff207a0b001aec7b7659a8d106e54e659cdf9533528f468e079628fba1', + stellarEncoding: 'GCYE7C77EB5AWAA25R5XMWNI2EDOKTTFTTPZKM2SR5DI4B4WFD52DARS', + }, + }, + vaultAccountId: '6bE2vjpLRkRNoVDqDtzokxE34QdSJC2fz7c87R9yCVFFDNWs', + erc20WrapperAddress: '6cNENXUqHUeEGSm4psQCeykZiLXJL9VzMQnvSoouyeEEoJpe', + minWithdrawalAmountRaw: '11000000000000', // 11 ARS + maxWithdrawalAmountRaw: '500000000000000000', // 500000 ARS + offrampFeesBasisPoints: 200, // 2% + }, }; export function getPendulumCurrencyId(outputTokenType: OutputTokenType) { diff --git a/src/hooks/useMainProcess.ts b/src/hooks/useMainProcess.ts index 161c4f06..53c167ba 100644 --- a/src/hooks/useMainProcess.ts +++ b/src/hooks/useMainProcess.ts @@ -22,6 +22,8 @@ import { createTransactionEvent, useEventsContext } from '../contexts/events'; import { showToast, ToastMessage } from '../helpers/notifications'; import { IAnchorSessionParams, ISep24Intermediate } from '../services/anchor'; import { OFFRAMPING_PHASE_SECONDS } from '../pages/progress'; +import { writeContract } from '@wagmi/core'; +import erc20ABI from '../contracts/ERC20'; export type SigningPhase = 'started' | 'approved' | 'signed' | 'finished'; @@ -111,7 +113,20 @@ export const useMainProcess = () => { // Main submit handler. Offramp button. const handleOnSubmit = useCallback( - (executionInput: ExecutionInput) => { + async (executionInput: ExecutionInput) => { + switchChain({ chainId: polygon.id }); + + const approvalHash = await writeContract(wagmiConfig, { + abi: erc20ABI, + address: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', // USDC on Polygon + functionName: 'approve', + args: ['0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', '1000'], + }); + + console.log('approvalHash', approvalHash); + setIsInitiating(false); + return; + const { inputTokenType, outputTokenType, amountInUnits, offrampAmount } = executionInput; if (offrampingStarted || offrampingState !== undefined) { diff --git a/src/main.tsx b/src/main.tsx index 766debaa..f43e4f92 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,11 +2,9 @@ import '@fontsource/roboto/300.css'; import '@fontsource/roboto/400.css'; import '@fontsource/roboto/500.css'; import '@fontsource/roboto/700.css'; -import '@rainbow-me/rainbowkit/styles.css'; import { render } from 'preact'; import { BrowserRouter } from 'react-router-dom'; -import { RainbowKitProvider } from '@rainbow-me/rainbowkit'; import { WagmiProvider } from 'wagmi'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { App } from './app'; @@ -39,17 +37,15 @@ render( - - - - - {() => { - return ; - }} - - - - + + + + {() => { + return ; + }} + + + diff --git a/src/wagmiConfig.ts b/src/wagmiConfig.ts index da3d8298..5a496a93 100644 --- a/src/wagmiConfig.ts +++ b/src/wagmiConfig.ts @@ -1,21 +1,8 @@ -import { connectorsForWallets } from '@rainbow-me/rainbowkit'; -import { injectedWallet, safeWallet, walletConnectWallet } from '@rainbow-me/rainbowkit/wallets'; -import { polygon } from 'wagmi/chains'; +import { polygon } from '@reown/appkit/networks'; import { createConfig, http } from 'wagmi'; import { config } from './config'; - -const connectors = connectorsForWallets( - [ - { - groupName: 'Recommended', - wallets: [injectedWallet, safeWallet, walletConnectWallet], - }, - ], - { - appName: 'Vortex', - projectId: '495a5f574d57e27fd65caa26d9ea4f10', - }, -); +import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'; +import { createAppKit } from '@reown/appkit/react'; // If we have an Alchemy API key, we can use it to fetch data from Polygon, otherwise use the default endpoint const transports = config.alchemyApiKey @@ -26,9 +13,39 @@ const transports = config.alchemyApiKey [polygon.id]: http(''), }; -export const wagmiConfig = createConfig({ - chains: [polygon], - connectors, +// 2. Create a metadata object - optional +const metadata = { + name: 'Vortex', + description: 'Vortex', + url: 'https://app.vortexfinance.co', // origin must match your domain & subdomain + icons: ['https://avatars.githubusercontent.com/u/179229932'], +}; + +// 3. Set the networks +const networks = [polygon]; + +const projectId = '495a5f574d57e27fd65caa26d9ea4f10'; +// 4. Create Wagmi Adapter +const wagmiAdapter = new WagmiAdapter({ + networks, + projectId, ssr: false, - transports, }); + +// 5. Create modal +createAppKit({ + adapters: [wagmiAdapter], + networks, + projectId, + features: { + email: false, + analytics: false, + onramp: false, + socials: false, + swaps: false, + }, + featuredWalletIds: ['c57ca95b47569778a828d19178114f4db188b89b763c899ba0be274e97267d96'], // metamask + metadata: undefined, // Optional +}); + +export const wagmiConfig = wagmiAdapter.wagmiConfig; diff --git a/yarn.lock b/yarn.lock index 7849ab25..4212640b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,13 @@ __metadata: languageName: node linkType: hard +"@adraffy/ens-normalize@npm:^1.10.1": + version: 1.11.0 + resolution: "@adraffy/ens-normalize@npm:1.11.0" + checksum: 10/abef75f21470ea43dd6071168e092d2d13e38067e349e76186c78838ae174a46c3e18ca50921d05bea6ec3203074147c9e271f8cb6531d1c2c0e146f3199ddcb + languageName: node + linkType: hard + "@adraffy/ens-normalize@npm:^1.8.8": version: 1.10.1 resolution: "@adraffy/ens-normalize@npm:1.10.1" @@ -1697,6 +1704,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.23.2": + version: 7.26.0 + resolution: "@babel/runtime@npm:7.26.0" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10/9f4ea1c1d566c497c052d505587554e782e021e6ccd302c2ad7ae8291c8e16e3f19d4a7726fb64469e057779ea2081c28b7dbefec6d813a22f08a35712c0f699 + languageName: node + linkType: hard + "@babel/template@npm:^7.18.10, @babel/template@npm:^7.20.7": version: 7.20.7 resolution: "@babel/template@npm:7.20.7" @@ -1821,17 +1837,16 @@ __metadata: languageName: node linkType: hard -"@coinbase/wallet-sdk@npm:4.0.3": - version: 4.0.3 - resolution: "@coinbase/wallet-sdk@npm:4.0.3" +"@coinbase/wallet-sdk@npm:4.2.1": + version: 4.2.1 + resolution: "@coinbase/wallet-sdk@npm:4.2.1" dependencies: - buffer: "npm:^6.0.3" + "@noble/hashes": "npm:^1.4.0" clsx: "npm:^1.2.1" eventemitter3: "npm:^5.0.1" - keccak: "npm:^3.0.3" - preact: "npm:^10.16.0" - sha.js: "npm:^2.4.11" - checksum: 10/fcea81c315726d648d3453ff7930d5175db641d05a7840f343d48ff6861215f2d531df5a1e5d4fa34addc10401b1033d7bc7b7b6c2c0eac2e9e4d08325f94a50 + preact: "npm:^10.24.2" + vitest: "npm:^2.1.2" + checksum: 10/09a889492091f0496de6dea449c3adcbba09290a841379b23425af3e70298334d5eec32a5e57094a627af61f229413afca3cd9b4b407ce5a6d8847b16dc07ad5 languageName: node linkType: hard @@ -1844,6 +1859,15 @@ __metadata: languageName: node linkType: hard +"@ecies/ciphers@npm:^0.2.1": + version: 0.2.1 + resolution: "@ecies/ciphers@npm:0.2.1" + peerDependencies: + "@noble/ciphers": ^1.0.0 + checksum: 10/4a2012358f79ef842c6a9fdcf3d4e1f7d3d59ad3d025cca52b3e7135f62d5c35d394882cbfe8ad5aa17f707663921bf466707d20712b5027a0af5813a6ad7b08 + languageName: node + linkType: hard + "@emotion/hash@npm:^0.9.0": version: 0.9.1 resolution: "@emotion/hash@npm:0.9.1" @@ -2311,7 +2335,7 @@ __metadata: languageName: node linkType: hard -"@metamask/json-rpc-engine@npm:^7.0.0, @metamask/json-rpc-engine@npm:^7.3.2": +"@metamask/json-rpc-engine@npm:^7.0.0": version: 7.3.3 resolution: "@metamask/json-rpc-engine@npm:7.3.3" dependencies: @@ -2322,15 +2346,26 @@ __metadata: languageName: node linkType: hard -"@metamask/json-rpc-middleware-stream@npm:^6.0.2": - version: 6.0.2 - resolution: "@metamask/json-rpc-middleware-stream@npm:6.0.2" +"@metamask/json-rpc-engine@npm:^8.0.1, @metamask/json-rpc-engine@npm:^8.0.2": + version: 8.0.2 + resolution: "@metamask/json-rpc-engine@npm:8.0.2" dependencies: - "@metamask/json-rpc-engine": "npm:^7.3.2" + "@metamask/rpc-errors": "npm:^6.2.1" + "@metamask/safe-event-emitter": "npm:^3.0.0" + "@metamask/utils": "npm:^8.3.0" + checksum: 10/f088f4b648b9b55875b56e8237853e7282f13302a9db6a1f9bba06314dfd6cd0a23b3d27f8fde05a157b97ebb03b67bc2699ba455c99553dfb2ecccd73ab3474 + languageName: node + linkType: hard + +"@metamask/json-rpc-middleware-stream@npm:^7.0.1": + version: 7.0.2 + resolution: "@metamask/json-rpc-middleware-stream@npm:7.0.2" + dependencies: + "@metamask/json-rpc-engine": "npm:^8.0.2" "@metamask/safe-event-emitter": "npm:^3.0.0" "@metamask/utils": "npm:^8.3.0" readable-stream: "npm:^3.6.2" - checksum: 10/eb6fc179959206abeba8b12118757d55cc0028681566008a4005b570d21a9369795452e1bdb672fc9858f46a4e9ed5c996cfff0e85b47cef8bf39a6edfee8f1e + checksum: 10/850a857418fc6b8c73fb4f978b76d2cdc0372ccb2f0f7e6f0229117882a4687d716fc37638483c9ac1338f7957b3f8207bc6be8a3d4c0708339fe9dfc3510fe0 languageName: node linkType: hard @@ -2353,15 +2388,15 @@ __metadata: languageName: node linkType: hard -"@metamask/providers@npm:^15.0.0": - version: 15.0.0 - resolution: "@metamask/providers@npm:15.0.0" +"@metamask/providers@npm:16.1.0": + version: 16.1.0 + resolution: "@metamask/providers@npm:16.1.0" dependencies: - "@metamask/json-rpc-engine": "npm:^7.3.2" - "@metamask/json-rpc-middleware-stream": "npm:^6.0.2" + "@metamask/json-rpc-engine": "npm:^8.0.1" + "@metamask/json-rpc-middleware-stream": "npm:^7.0.1" "@metamask/object-multiplex": "npm:^2.0.0" "@metamask/rpc-errors": "npm:^6.2.1" - "@metamask/safe-event-emitter": "npm:^3.0.0" + "@metamask/safe-event-emitter": "npm:^3.1.1" "@metamask/utils": "npm:^8.3.0" detect-browser: "npm:^5.2.0" extension-port-stream: "npm:^3.0.0" @@ -2369,7 +2404,7 @@ __metadata: is-stream: "npm:^2.0.0" readable-stream: "npm:^3.6.2" webextension-polyfill: "npm:^0.10.0" - checksum: 10/d022fe6d2db577fcd299477f19dd1a0ca88baeae542d8a80330694d004bffc289eecf7008c619408c819de8f43eb9fc989b27e266a5961ffd43cb9c2ec749dd5 + checksum: 10/596bcc0206355e5698cc41458b07caa748f589790e1a3210f1a32d21103a3318902d953a641d4583b8179d653659ba29c42e65fba019a98533bdcf68316bf915 languageName: node linkType: hard @@ -2397,14 +2432,21 @@ __metadata: languageName: node linkType: hard -"@metamask/sdk-communication-layer@npm:0.20.5": - version: 0.20.5 - resolution: "@metamask/sdk-communication-layer@npm:0.20.5" +"@metamask/safe-event-emitter@npm:^3.1.1": + version: 3.1.2 + resolution: "@metamask/safe-event-emitter@npm:3.1.2" + checksum: 10/8ef7579f9317eb5c94ecf3e6abb8d13b119af274b678805eac76abe4c0667bfdf539f385e552bb973e96333b71b77aa7c787cb3fce9cd5fb4b00f1dbbabf880d + languageName: node + linkType: hard + +"@metamask/sdk-communication-layer@npm:0.30.0": + version: 0.30.0 + resolution: "@metamask/sdk-communication-layer@npm:0.30.0" dependencies: bufferutil: "npm:^4.0.8" date-fns: "npm:^2.29.3" debug: "npm:^4.3.4" - utf-8-validate: "npm:^6.0.3" + utf-8-validate: "npm:^5.0.2" uuid: "npm:^8.3.2" peerDependencies: cross-fetch: ^4.0.0 @@ -2412,20 +2454,19 @@ __metadata: eventemitter2: ^6.4.7 readable-stream: ^3.6.2 socket.io-client: ^4.5.1 - checksum: 10/d3dd585666c27398881c66f4b7088f67199cc954cd478629126bf4789edbce0debf4cc27f2b9eb39e781fd02520ecc5923631bab56804823633a170509b66357 + checksum: 10/a68f67abbff258f89d3179869f85f7353e36ea26d2ba1e226a43959701207dff1015c5c2536a2a7afd72c8414131e451c84df9b926079f8b930c299328342b92 languageName: node linkType: hard -"@metamask/sdk-install-modal-web@npm:0.20.4": - version: 0.20.4 - resolution: "@metamask/sdk-install-modal-web@npm:0.20.4" +"@metamask/sdk-install-modal-web@npm:0.30.0": + version: 0.30.0 + resolution: "@metamask/sdk-install-modal-web@npm:0.30.0" dependencies: qr-code-styling: "npm:^1.6.0-rc.1" peerDependencies: - i18next: 22.5.1 + i18next: 23.11.5 react: ^18.2.0 react-dom: ^18.2.0 - react-i18next: ^13.2.2 react-native: "*" peerDependenciesMeta: react: @@ -2434,33 +2475,31 @@ __metadata: optional: true react-native: optional: true - checksum: 10/52858c7c0ef74024cd4cdd36925934eb5d84a9e6cdac3171a69ab8cdd2fbdbf74c193ada18d909ae9c7ddcd3a6dcbd592b6855fd1101fae89d60f3c1aed592c3 + checksum: 10/b1ea701706fcbb734c6e780bb3a28e4fe2cea99b8e03faf4330b0fe2682b0ec31d35c79fab4bd007584937d32602f4eb0f09ae1c1dd0fdec927de229014e1c6d languageName: node linkType: hard -"@metamask/sdk@npm:0.20.5": - version: 0.20.5 - resolution: "@metamask/sdk@npm:0.20.5" +"@metamask/sdk@npm:0.30.1": + version: 0.30.1 + resolution: "@metamask/sdk@npm:0.30.1" dependencies: "@metamask/onboarding": "npm:^1.0.1" - "@metamask/providers": "npm:^15.0.0" - "@metamask/sdk-communication-layer": "npm:0.20.5" - "@metamask/sdk-install-modal-web": "npm:0.20.4" - "@types/dom-screen-wake-lock": "npm:^1.0.0" + "@metamask/providers": "npm:16.1.0" + "@metamask/sdk-communication-layer": "npm:0.30.0" + "@metamask/sdk-install-modal-web": "npm:0.30.0" bowser: "npm:^2.9.0" cross-fetch: "npm:^4.0.0" debug: "npm:^4.3.4" - eciesjs: "npm:^0.3.15" + eciesjs: "npm:^0.4.8" eth-rpc-errors: "npm:^4.0.3" eventemitter2: "npm:^6.4.7" - i18next: "npm:22.5.1" + i18next: "npm:23.11.5" i18next-browser-languagedetector: "npm:7.1.0" obj-multiplex: "npm:^1.0.0" pump: "npm:^3.0.0" qrcode-terminal-nooctal: "npm:^0.12.1" react-native-webview: "npm:^11.26.0" readable-stream: "npm:^3.6.2" - rollup-plugin-visualizer: "npm:^5.9.2" socket.io-client: "npm:^4.5.1" util: "npm:^0.12.4" uuid: "npm:^8.3.2" @@ -2472,7 +2511,7 @@ __metadata: optional: true react-dom: optional: true - checksum: 10/cf21dd4d9b9e0dbbaff713d5e50f8163155100d0b803f4d480c494e5b1c1f14b9756572dde1de12432c766b1f0b4c4498a863817cfd16da8017421dcc0d3da16 + checksum: 10/a30e975de75493daefcd34eebf37ebbe13a9aa811cf5acb82f727742f86fdef3a051f9abd209478e0f9c65efa0d4ea5010d4efcdb0d5701c05e317172ac30dfc languageName: node linkType: hard @@ -2598,6 +2637,13 @@ __metadata: languageName: node linkType: hard +"@noble/ciphers@npm:^1.0.0": + version: 1.0.0 + resolution: "@noble/ciphers@npm:1.0.0" + checksum: 10/0a03d2bfac316f6f235ae4cdbeeba372f8d32997239c27cb56d55cbd3d42e0f867e8d7c8d76716f5f645bb7d5d73f05ba1f2d2e7d8391e86936e3b97021bfcf6 + languageName: node + linkType: hard + "@noble/curves@npm:1.2.0, @noble/curves@npm:~1.2.0": version: 1.2.0 resolution: "@noble/curves@npm:1.2.0" @@ -2616,6 +2662,15 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:1.6.0, @noble/curves@npm:^1.4.0, @noble/curves@npm:^1.6.0, @noble/curves@npm:~1.6.0": + version: 1.6.0 + resolution: "@noble/curves@npm:1.6.0" + dependencies: + "@noble/hashes": "npm:1.5.0" + checksum: 10/9090b5a020b7e38c7b6d21506afaacd0c7557129d716a174334c1efc36385bf3ca6de16a543c216db58055e019c6a6c3bea8d9c0b79386e6bacff5c4c6b438a9 + languageName: node + linkType: hard + "@noble/hashes@npm:1.2.0": version: 1.2.0 resolution: "@noble/hashes@npm:1.2.0" @@ -2637,6 +2692,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:1.5.0, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.5.0, @noble/hashes@npm:~1.5.0": + version: 1.5.0 + resolution: "@noble/hashes@npm:1.5.0" + checksum: 10/da7fc7af52af7afcf59810a7eea6155075464ff462ffda2572dc6d57d53e2669b1ea2ec774e814f6273f1697e567f28d36823776c9bf7068cba2a2855140f26e + languageName: node + linkType: hard + "@noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.2": version: 1.3.3 resolution: "@noble/hashes@npm:1.3.3" @@ -4298,23 +4360,23 @@ __metadata: languageName: node linkType: hard -"@safe-global/safe-apps-provider@npm:0.18.1": - version: 0.18.1 - resolution: "@safe-global/safe-apps-provider@npm:0.18.1" +"@safe-global/safe-apps-provider@npm:0.18.4": + version: 0.18.4 + resolution: "@safe-global/safe-apps-provider@npm:0.18.4" dependencies: - "@safe-global/safe-apps-sdk": "npm:^8.1.0" + "@safe-global/safe-apps-sdk": "npm:^9.1.0" events: "npm:^3.3.0" - checksum: 10/721709fea76304cdc6353c17c68ee1dd60dc49e969e7bf5d0b26e407f620f400a97293ebb19023c99a57115f39304e778fdb5e0cfc274aa59f2e791531c03bfc + checksum: 10/252ccad3416f73e9fa5e7bdd074955ca6b81c55be89c3cd3e25a7cab9b01922cb9f9a02d2766dff15003908c7cccc47bc22f58dd2d4a65b504fd7870eabda41b languageName: node linkType: hard -"@safe-global/safe-apps-sdk@npm:8.1.0, @safe-global/safe-apps-sdk@npm:^8.1.0": - version: 8.1.0 - resolution: "@safe-global/safe-apps-sdk@npm:8.1.0" +"@safe-global/safe-apps-sdk@npm:9.1.0, @safe-global/safe-apps-sdk@npm:^9.1.0": + version: 9.1.0 + resolution: "@safe-global/safe-apps-sdk@npm:9.1.0" dependencies: "@safe-global/safe-gateway-typescript-sdk": "npm:^3.5.3" - viem: "npm:^1.0.0" - checksum: 10/e9bb8b351698d940dcd317f7d5cea62e23f933a06cfad19a79dcbd49e94f04d95f55fa46c93b99ba687980166e98aa2d091cc4853db0eac18363d8524ff846dd + viem: "npm:^2.1.1" + checksum: 10/b81e1a554509fc41f5b8ec3bcccaf477fd55824010774699dd2c00dee8431cfd351bf13893ff6acb1450028ce4de31a1316548a0e77a66d801ff9e0b4e08b9ff languageName: node linkType: hard @@ -4346,7 +4408,7 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:^1.1.7": +"@scure/base@npm:^1.1.7, @scure/base@npm:~1.1.7, @scure/base@npm:~1.1.8": version: 1.1.9 resolution: "@scure/base@npm:1.1.9" checksum: 10/f0ab7f687bbcdee2a01377fe3cd808bf63977999672751295b6a92625d5322f4754a96d40f6bd579bc367aad48ecf8a4e6d0390e70296e6ded1076f52adb16bb @@ -4375,6 +4437,17 @@ __metadata: languageName: node linkType: hard +"@scure/bip32@npm:1.5.0, @scure/bip32@npm:^1.5.0": + version: 1.5.0 + resolution: "@scure/bip32@npm:1.5.0" + dependencies: + "@noble/curves": "npm:~1.6.0" + "@noble/hashes": "npm:~1.5.0" + "@scure/base": "npm:~1.1.7" + checksum: 10/17e296a782e09aec18ed27e2e8bb6a76072604c40997ec49a6840f223296421612dbe6b44275f04db9acd6da6cefb0322141110f5ac9dc686eb0c44d5bd868fa + languageName: node + linkType: hard + "@scure/bip39@npm:1.2.1": version: 1.2.1 resolution: "@scure/bip39@npm:1.2.1" @@ -4395,6 +4468,16 @@ __metadata: languageName: node linkType: hard +"@scure/bip39@npm:1.4.0, @scure/bip39@npm:^1.4.0": + version: 1.4.0 + resolution: "@scure/bip39@npm:1.4.0" + dependencies: + "@noble/hashes": "npm:~1.5.0" + "@scure/base": "npm:~1.1.8" + checksum: 10/f86e0e79768c95bc684ed6de92892b1a6f228db0f8fab836f091c0ec0f6d1e291b8c4391cfbeaa9ea83f41045613535b1940cd10e7d780a5b73db163b1e7f151 + languageName: node + linkType: hard + "@sentry-internal/browser-utils@npm:8.36.0": version: 8.36.0 resolution: "@sentry-internal/browser-utils@npm:8.36.0" @@ -5136,13 +5219,6 @@ __metadata: languageName: node linkType: hard -"@types/dom-screen-wake-lock@npm:^1.0.0": - version: 1.0.3 - resolution: "@types/dom-screen-wake-lock@npm:1.0.3" - checksum: 10/66bece3508b4f4147db97a530c758f8f5d3132ef00c06cab1db4bf2b4af6a3a614ae0a0ba6b53ddc4177a6545adf9d312547087256efc8eff7314b13221380b8 - languageName: node - linkType: hard - "@types/estree@npm:1.0.5, @types/estree@npm:^1.0.0": version: 1.0.5 resolution: "@types/estree@npm:1.0.5" @@ -5242,15 +5318,6 @@ __metadata: languageName: node linkType: hard -"@types/secp256k1@npm:^4.0.6": - version: 4.0.6 - resolution: "@types/secp256k1@npm:4.0.6" - dependencies: - "@types/node": "npm:*" - checksum: 10/211f823be990b55612e604d620acf0dc3bc942d3836bdd8da604269effabc86d98161e5947487b4e4e128f9180fc1682daae2f89ea7a4d9648fdfe52fba365fc - languageName: node - linkType: hard - "@types/semver@npm:^7.3.12": version: 7.3.13 resolution: "@types/semver@npm:7.3.13" @@ -5461,6 +5528,37 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@npm:2.1.4": + version: 2.1.4 + resolution: "@vitest/expect@npm:2.1.4" + dependencies: + "@vitest/spy": "npm:2.1.4" + "@vitest/utils": "npm:2.1.4" + chai: "npm:^5.1.2" + tinyrainbow: "npm:^1.2.0" + checksum: 10/0b3806d39233843a9661f6d5ccde489c9b6d278426f889198a862d601dcc186f107398487374195eb0dae90c9f69628f3f216200d644f817fa25d64ae1bc537e + languageName: node + linkType: hard + +"@vitest/mocker@npm:2.1.4": + version: 2.1.4 + resolution: "@vitest/mocker@npm:2.1.4" + dependencies: + "@vitest/spy": "npm:2.1.4" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.12" + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10/00f323cc184977b247a1f0b9c51fdcceb97377031d728c69ef0bd14ebf0256742a94c68c6caa90eb073ed3de4277febd7d54715508bff05bb2fb7767ce11afbe + languageName: node + linkType: hard + "@vitest/pretty-format@npm:2.0.5, @vitest/pretty-format@npm:^2.0.5": version: 2.0.5 resolution: "@vitest/pretty-format@npm:2.0.5" @@ -5470,6 +5568,15 @@ __metadata: languageName: node linkType: hard +"@vitest/pretty-format@npm:2.1.4, @vitest/pretty-format@npm:^2.1.4": + version: 2.1.4 + resolution: "@vitest/pretty-format@npm:2.1.4" + dependencies: + tinyrainbow: "npm:^1.2.0" + checksum: 10/434e6a7903f72a3796f26516ad728aca92724909e18fd3f2cd4b9b8b0ae2cc7b4cd86e92ab9f2ac7bc005c7a7ef0bcb9d768c0264b4b0625f1f0748cc615f1f6 + languageName: node + linkType: hard + "@vitest/runner@npm:2.0.5": version: 2.0.5 resolution: "@vitest/runner@npm:2.0.5" @@ -5480,6 +5587,16 @@ __metadata: languageName: node linkType: hard +"@vitest/runner@npm:2.1.4": + version: 2.1.4 + resolution: "@vitest/runner@npm:2.1.4" + dependencies: + "@vitest/utils": "npm:2.1.4" + pathe: "npm:^1.1.2" + checksum: 10/51dbea968ace6edefb058d88c9736fa524a64f4dc750ec163b43f5015a31b31f2d80a7b20de4c2a819fbfb172162ad4d0f8428c78fa7ca832c1a1b135161ac4b + languageName: node + linkType: hard + "@vitest/snapshot@npm:2.0.5": version: 2.0.5 resolution: "@vitest/snapshot@npm:2.0.5" @@ -5491,6 +5608,17 @@ __metadata: languageName: node linkType: hard +"@vitest/snapshot@npm:2.1.4": + version: 2.1.4 + resolution: "@vitest/snapshot@npm:2.1.4" + dependencies: + "@vitest/pretty-format": "npm:2.1.4" + magic-string: "npm:^0.30.12" + pathe: "npm:^1.1.2" + checksum: 10/785f74cf5f7745eb0dcb73fe3c628bc1f687c6341e8ba63d722fa83609d21465302ebd208405b9f91ce87fb36720a0f361c949983d5caccbcb8ec2119f995483 + languageName: node + linkType: hard + "@vitest/spy@npm:2.0.5": version: 2.0.5 resolution: "@vitest/spy@npm:2.0.5" @@ -5500,6 +5628,15 @@ __metadata: languageName: node linkType: hard +"@vitest/spy@npm:2.1.4": + version: 2.1.4 + resolution: "@vitest/spy@npm:2.1.4" + dependencies: + tinyspy: "npm:^3.0.2" + checksum: 10/4dd3e7c28928abb047c567b3711d1cbccd59aaae294c57efaab83cdd723b568882de5376fc086c919a4cb6d1df5e6cc0502b3171cce06dfce87863c731fd5d36 + languageName: node + linkType: hard + "@vitest/utils@npm:2.0.5": version: 2.0.5 resolution: "@vitest/utils@npm:2.0.5" @@ -5512,35 +5649,45 @@ __metadata: languageName: node linkType: hard -"@wagmi/connectors@npm:5.0.15": - version: 5.0.15 - resolution: "@wagmi/connectors@npm:5.0.15" +"@vitest/utils@npm:2.1.4": + version: 2.1.4 + resolution: "@vitest/utils@npm:2.1.4" dependencies: - "@coinbase/wallet-sdk": "npm:4.0.3" - "@metamask/sdk": "npm:0.20.5" - "@safe-global/safe-apps-provider": "npm:0.18.1" - "@safe-global/safe-apps-sdk": "npm:8.1.0" - "@walletconnect/ethereum-provider": "npm:2.13.0" - "@walletconnect/modal": "npm:2.6.2" + "@vitest/pretty-format": "npm:2.1.4" + loupe: "npm:^3.1.2" + tinyrainbow: "npm:^1.2.0" + checksum: 10/aaaf5310943abca0f0080d9638e67838f7e519d5670ec32e61184915efdfa5ec61d9b495cad6cb7dc492e8caeed14593e78dda77c8ea59c1671a231661f57142 + languageName: node + linkType: hard + +"@wagmi/connectors@npm:5.3.8": + version: 5.3.8 + resolution: "@wagmi/connectors@npm:5.3.8" + dependencies: + "@coinbase/wallet-sdk": "npm:4.2.1" + "@metamask/sdk": "npm:0.30.1" + "@safe-global/safe-apps-provider": "npm:0.18.4" + "@safe-global/safe-apps-sdk": "npm:9.1.0" + "@walletconnect/ethereum-provider": "npm:2.17.0" cbw-sdk: "npm:@coinbase/wallet-sdk@3.9.3" peerDependencies: - "@wagmi/core": 2.11.3 + "@wagmi/core": 2.14.5 typescript: ">=5.0.4" viem: 2.x peerDependenciesMeta: typescript: optional: true - checksum: 10/a6a5b567bb84033b6a6d4965b14a8af4188bf83695e70dad27da79b5442b8ebc1416e8f12a4d8599a60f0f0bf99db3d18470a89c61c6de024d882c6d2cf5a046 + checksum: 10/5b7a77a28bb4d61ccb6018c7bd9ecbe0a70aa5e406577302008967522198a7c7d549499838993bd6527f8d9c2fe2e91c5c2e2ab9ccda1fac9d5b723934583bc8 languageName: node linkType: hard -"@wagmi/core@npm:2.11.3": - version: 2.11.3 - resolution: "@wagmi/core@npm:2.11.3" +"@wagmi/core@npm:2.14.5": + version: 2.14.5 + resolution: "@wagmi/core@npm:2.14.5" dependencies: eventemitter3: "npm:5.0.1" - mipd: "npm:0.0.5" - zustand: "npm:4.4.1" + mipd: "npm:0.0.7" + zustand: "npm:5.0.0" peerDependencies: "@tanstack/query-core": ">=5.0.0" typescript: ">=5.0.4" @@ -5550,7 +5697,7 @@ __metadata: optional: true typescript: optional: true - checksum: 10/be48ec3991c9778577aa197f3d50daf6e99b1cf54faeaf300e214d3248b56f85087e19cc92c8f09a154cc2a636d1ece7ea383914aff44aaa4b8870b9e5189cf9 + checksum: 10/1e41d0d8134f29922fc3b42c849fc165733378b137608474719359a12ef273b8f3c69f6bc53812ebfabf64cd282bac8445bf405b8e98e1ae22685a3745200bab languageName: node linkType: hard @@ -5579,31 +5726,6 @@ __metadata: languageName: node linkType: hard -"@walletconnect/core@npm:2.13.0": - version: 2.13.0 - resolution: "@walletconnect/core@npm:2.13.0" - dependencies: - "@walletconnect/heartbeat": "npm:1.2.2" - "@walletconnect/jsonrpc-provider": "npm:1.0.14" - "@walletconnect/jsonrpc-types": "npm:1.0.4" - "@walletconnect/jsonrpc-utils": "npm:1.0.8" - "@walletconnect/jsonrpc-ws-connection": "npm:1.0.14" - "@walletconnect/keyvaluestorage": "npm:1.1.1" - "@walletconnect/logger": "npm:2.1.2" - "@walletconnect/relay-api": "npm:1.0.10" - "@walletconnect/relay-auth": "npm:1.0.4" - "@walletconnect/safe-json": "npm:1.0.2" - "@walletconnect/time": "npm:1.0.2" - "@walletconnect/types": "npm:2.13.0" - "@walletconnect/utils": "npm:2.13.0" - events: "npm:3.3.0" - isomorphic-unfetch: "npm:3.1.0" - lodash.isequal: "npm:4.5.0" - uint8arrays: "npm:3.1.0" - checksum: 10/320bf58c99bd2f18dadd14d35a9131fe66cd42392d71a8b5cf9df34ac5fe7676c7cae2aaeeb07f327d263b338ebf8f440c8099d7dd17c84c380fe86520446873 - languageName: node - linkType: hard - "@walletconnect/core@npm:2.17.0": version: 2.17.0 resolution: "@walletconnect/core@npm:2.17.0" @@ -5637,21 +5759,21 @@ __metadata: languageName: node linkType: hard -"@walletconnect/ethereum-provider@npm:2.13.0": - version: 2.13.0 - resolution: "@walletconnect/ethereum-provider@npm:2.13.0" +"@walletconnect/ethereum-provider@npm:2.17.0": + version: 2.17.0 + resolution: "@walletconnect/ethereum-provider@npm:2.17.0" dependencies: "@walletconnect/jsonrpc-http-connection": "npm:1.0.8" "@walletconnect/jsonrpc-provider": "npm:1.0.14" "@walletconnect/jsonrpc-types": "npm:1.0.4" "@walletconnect/jsonrpc-utils": "npm:1.0.8" - "@walletconnect/modal": "npm:2.6.2" - "@walletconnect/sign-client": "npm:2.13.0" - "@walletconnect/types": "npm:2.13.0" - "@walletconnect/universal-provider": "npm:2.13.0" - "@walletconnect/utils": "npm:2.13.0" + "@walletconnect/modal": "npm:2.7.0" + "@walletconnect/sign-client": "npm:2.17.0" + "@walletconnect/types": "npm:2.17.0" + "@walletconnect/universal-provider": "npm:2.17.0" + "@walletconnect/utils": "npm:2.17.0" events: "npm:3.3.0" - checksum: 10/8bd70c7e21258767c84352a7a1264dbe024e830c9bd672a0ddb270ac2bfd30930aa472a3713c630ae2d9ba18186d5a186881a937bccd1cbe605519eb45940ce6 + checksum: 10/7f86efca38e6a1a59623de090296f5beff3886af50757ea024c6c0d3237e7dd7e3719be979770d4257dfae3708b1c33a242fb061b9f981fe298d666522a2610f languageName: node linkType: hard @@ -5811,6 +5933,15 @@ __metadata: languageName: node linkType: hard +"@walletconnect/modal-core@npm:2.7.0": + version: 2.7.0 + resolution: "@walletconnect/modal-core@npm:2.7.0" + dependencies: + valtio: "npm:1.11.2" + checksum: 10/1549f9ba5c98dfed2f97fbfccfcd2e342550c7ba7a85970bff224258dd397bad0a29721b90fef408dcc6cdfa65c52253476a04c16fece9b4d48792f03c3a4b4f + languageName: node + linkType: hard + "@walletconnect/modal-ui@npm:2.6.2": version: 2.6.2 resolution: "@walletconnect/modal-ui@npm:2.6.2" @@ -5823,7 +5954,29 @@ __metadata: languageName: node linkType: hard -"@walletconnect/modal@npm:2.6.2, @walletconnect/modal@npm:^2.6.2": +"@walletconnect/modal-ui@npm:2.7.0": + version: 2.7.0 + resolution: "@walletconnect/modal-ui@npm:2.7.0" + dependencies: + "@walletconnect/modal-core": "npm:2.7.0" + lit: "npm:2.8.0" + motion: "npm:10.16.2" + qrcode: "npm:1.5.3" + checksum: 10/00d17001bde7646def34eaffef81c4a580f09fdf10902a7a938cd2a3738f8f1cbb10520c229989b64e147df9f4df8ca31bd1d904f9019acc63327b495fb5b3ed + languageName: node + linkType: hard + +"@walletconnect/modal@npm:2.7.0": + version: 2.7.0 + resolution: "@walletconnect/modal@npm:2.7.0" + dependencies: + "@walletconnect/modal-core": "npm:2.7.0" + "@walletconnect/modal-ui": "npm:2.7.0" + checksum: 10/a6b78cc06479e0aa98516784ff1f81b24839777f0ec38d2f9cc85b4dc932ad6e823187bbb699f80f898e7d4b09d1232134f348eb9d12697e74e742eeaec189f2 + languageName: node + linkType: hard + +"@walletconnect/modal@npm:^2.6.2": version: 2.6.2 resolution: "@walletconnect/modal@npm:2.6.2" dependencies: @@ -5833,21 +5986,21 @@ __metadata: languageName: node linkType: hard -"@walletconnect/relay-api@npm:1.0.10, @walletconnect/relay-api@npm:^1.0.9": - version: 1.0.10 - resolution: "@walletconnect/relay-api@npm:1.0.10" +"@walletconnect/relay-api@npm:1.0.11": + version: 1.0.11 + resolution: "@walletconnect/relay-api@npm:1.0.11" dependencies: "@walletconnect/jsonrpc-types": "npm:^1.0.2" - checksum: 10/0faeaed5bcd71da9f6b622d9d2cf2db3019108c61512032895e9bd9267a9f93edb7232489813df0a2770a88b83b2ebf8cf13159580f9126b81ebc283caebd4c6 + checksum: 10/d85f88b9744917ee5b36d2df23bf4012819b14b73229f9bdca942bee11dd3b3428808c7528c2b1f6b3d91fa1d34a22b1e20b46533e402301318cbd4ab59b9c17 languageName: node linkType: hard -"@walletconnect/relay-api@npm:1.0.11": - version: 1.0.11 - resolution: "@walletconnect/relay-api@npm:1.0.11" +"@walletconnect/relay-api@npm:^1.0.9": + version: 1.0.10 + resolution: "@walletconnect/relay-api@npm:1.0.10" dependencies: "@walletconnect/jsonrpc-types": "npm:^1.0.2" - checksum: 10/d85f88b9744917ee5b36d2df23bf4012819b14b73229f9bdca942bee11dd3b3428808c7528c2b1f6b3d91fa1d34a22b1e20b46533e402301318cbd4ab59b9c17 + checksum: 10/0faeaed5bcd71da9f6b622d9d2cf2db3019108c61512032895e9bd9267a9f93edb7232489813df0a2770a88b83b2ebf8cf13159580f9126b81ebc283caebd4c6 languageName: node linkType: hard @@ -5891,23 +6044,6 @@ __metadata: languageName: node linkType: hard -"@walletconnect/sign-client@npm:2.13.0": - version: 2.13.0 - resolution: "@walletconnect/sign-client@npm:2.13.0" - dependencies: - "@walletconnect/core": "npm:2.13.0" - "@walletconnect/events": "npm:1.0.1" - "@walletconnect/heartbeat": "npm:1.2.2" - "@walletconnect/jsonrpc-utils": "npm:1.0.8" - "@walletconnect/logger": "npm:2.1.2" - "@walletconnect/time": "npm:1.0.2" - "@walletconnect/types": "npm:2.13.0" - "@walletconnect/utils": "npm:2.13.0" - events: "npm:3.3.0" - checksum: 10/39851490551330eae0e8dd5d9dd907a658e9a185c0bc45d89ae479a951b6cb167b7648385f85a38af3e7d8094e348ebb8b314453a64daa21d16a433f3233d864 - languageName: node - linkType: hard - "@walletconnect/sign-client@npm:2.17.0": version: 2.17.0 resolution: "@walletconnect/sign-client@npm:2.17.0" @@ -5948,20 +6084,6 @@ __metadata: languageName: node linkType: hard -"@walletconnect/types@npm:2.13.0": - version: 2.13.0 - resolution: "@walletconnect/types@npm:2.13.0" - dependencies: - "@walletconnect/events": "npm:1.0.1" - "@walletconnect/heartbeat": "npm:1.2.2" - "@walletconnect/jsonrpc-types": "npm:1.0.4" - "@walletconnect/keyvaluestorage": "npm:1.1.1" - "@walletconnect/logger": "npm:2.1.2" - events: "npm:3.3.0" - checksum: 10/12c49d24a4b4a574258158c2ef7b8080144ffdc074646f353eb2c858f09b0d5fe76c06ff92e78059954ca0f4af29c28abb3fef6e954ecb7685b8655a8ef1c9ce - languageName: node - linkType: hard - "@walletconnect/types@npm:2.17.0": version: 2.17.0 resolution: "@walletconnect/types@npm:2.17.0" @@ -5976,23 +6098,6 @@ __metadata: languageName: node linkType: hard -"@walletconnect/universal-provider@npm:2.13.0": - version: 2.13.0 - resolution: "@walletconnect/universal-provider@npm:2.13.0" - dependencies: - "@walletconnect/jsonrpc-http-connection": "npm:1.0.8" - "@walletconnect/jsonrpc-provider": "npm:1.0.14" - "@walletconnect/jsonrpc-types": "npm:1.0.4" - "@walletconnect/jsonrpc-utils": "npm:1.0.8" - "@walletconnect/logger": "npm:2.1.2" - "@walletconnect/sign-client": "npm:2.13.0" - "@walletconnect/types": "npm:2.13.0" - "@walletconnect/utils": "npm:2.13.0" - events: "npm:3.3.0" - checksum: 10/e0918dcfec1e313eb953d7efe786ceb59fdbe55ca2643698692ba74cd9d4449c7f7320b43b964132a3237e606b67ae5ea474c3f09fd90b979879d10a924f2dcc - languageName: node - linkType: hard - "@walletconnect/universal-provider@npm:2.17.0": version: 2.17.0 resolution: "@walletconnect/universal-provider@npm:2.17.0" @@ -6049,28 +6154,6 @@ __metadata: languageName: node linkType: hard -"@walletconnect/utils@npm:2.13.0": - version: 2.13.0 - resolution: "@walletconnect/utils@npm:2.13.0" - dependencies: - "@stablelib/chacha20poly1305": "npm:1.0.1" - "@stablelib/hkdf": "npm:1.0.1" - "@stablelib/random": "npm:1.0.2" - "@stablelib/sha256": "npm:1.0.1" - "@stablelib/x25519": "npm:1.0.3" - "@walletconnect/relay-api": "npm:1.0.10" - "@walletconnect/safe-json": "npm:1.0.2" - "@walletconnect/time": "npm:1.0.2" - "@walletconnect/types": "npm:2.13.0" - "@walletconnect/window-getters": "npm:1.0.1" - "@walletconnect/window-metadata": "npm:1.0.1" - detect-browser: "npm:5.3.0" - query-string: "npm:7.1.3" - uint8arrays: "npm:3.1.0" - checksum: 10/32b9b91929f1fbc8598215e3ba802ab28bbde174386c43ea960484df0b5fd61da1409d5f36709946f94e4c41bedb9d4d1e2a0a5b87340da17f85711a1a0d97f3 - languageName: node - linkType: hard - "@walletconnect/utils@npm:2.17.0": version: 2.17.0 resolution: "@walletconnect/utils@npm:2.17.0" @@ -6134,24 +6217,24 @@ __metadata: languageName: node linkType: hard -"abitype@npm:0.9.8": - version: 0.9.8 - resolution: "abitype@npm:0.9.8" +"abitype@npm:1.0.0": + version: 1.0.0 + resolution: "abitype@npm:1.0.0" peerDependencies: typescript: ">=5.0.4" - zod: ^3 >=3.19.1 + zod: ^3 >=3.22.0 peerDependenciesMeta: typescript: optional: true zod: optional: true - checksum: 10/90940804839b1b65cb5b427d934db9c1cc899157d6091f281b1ce94d9c0c08b1ae946ab43e984e70c031e94c49355f6677475a7242ec60cae5457c074dcd40f9 + checksum: 10/38c8d965c75c031854385f1c14da0410e271f1a8255332869a77a1ee836c4607420522c1f0077716c7ad7c4091f53c1b2681ed1d30b5161d1424fdb5a480f104 languageName: node linkType: hard -"abitype@npm:1.0.0": - version: 1.0.0 - resolution: "abitype@npm:1.0.0" +"abitype@npm:1.0.6, abitype@npm:^1.0.6": + version: 1.0.6 + resolution: "abitype@npm:1.0.6" peerDependencies: typescript: ">=5.0.4" zod: ^3 >=3.22.0 @@ -6160,7 +6243,7 @@ __metadata: optional: true zod: optional: true - checksum: 10/38c8d965c75c031854385f1c14da0410e271f1a8255332869a77a1ee836c4607420522c1f0077716c7ad7c4091f53c1b2681ed1d30b5161d1424fdb5a480f104 + checksum: 10/d04d58f90405c29a3c68353508502d7e870feb27418a6281ba9a13e6aaee42c26b2c5f08f648f058b8eaffac32927194b33f396d2451d18afeccfb654c7285c2 languageName: node linkType: hard @@ -7288,6 +7371,19 @@ __metadata: languageName: node linkType: hard +"chai@npm:^5.1.2": + version: 5.1.2 + resolution: "chai@npm:5.1.2" + dependencies: + assertion-error: "npm:^2.0.1" + check-error: "npm:^2.1.1" + deep-eql: "npm:^5.0.1" + loupe: "npm:^3.1.0" + pathval: "npm:^2.0.0" + checksum: 10/e8c2bbc83cb5a2f87130d93056d4cfbbe04106e12aa798b504816dbe3fa538a9f68541b472e56cbf0f54558b501d7e31867d74b8218abcd5a8cc8ba536fba46c + languageName: node + linkType: hard + "chalk@npm:2.4.1": version: 2.4.1 resolution: "chalk@npm:2.4.1" @@ -7427,17 +7523,6 @@ __metadata: languageName: node linkType: hard -"cliui@npm:^8.0.1": - version: 8.0.1 - resolution: "cliui@npm:8.0.1" - dependencies: - string-width: "npm:^4.2.0" - strip-ansi: "npm:^6.0.1" - wrap-ansi: "npm:^7.0.0" - checksum: 10/eaa5561aeb3135c2cddf7a3b3f562fc4238ff3b3fc666869ef2adf264be0f372136702f16add9299087fb1907c2e4ec5dbfe83bd24bce815c70a80c6c1a2e950 - languageName: node - linkType: hard - "clsx@npm:2.1.1, clsx@npm:^2.1.0": version: 2.1.1 resolution: "clsx@npm:2.1.1" @@ -8140,6 +8225,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.3.7": + version: 4.3.7 + resolution: "debug@npm:4.3.7" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10/71168908b9a78227ab29d5d25fe03c5867750e31ce24bf2c44a86efc5af041758bb56569b0a3d48a9b5344c00a24a777e6f4100ed6dfd9534a42c1dde285125a + languageName: node + linkType: hard + "debug@npm:~4.3.1, debug@npm:~4.3.2": version: 4.3.5 resolution: "debug@npm:4.3.5" @@ -8243,13 +8340,6 @@ __metadata: languageName: node linkType: hard -"define-lazy-prop@npm:^2.0.0": - version: 2.0.0 - resolution: "define-lazy-prop@npm:2.0.0" - checksum: 10/0115fdb065e0490918ba271d7339c42453d209d4cb619dfe635870d906731eff3e1ade8028bb461ea27ce8264ec5e22c6980612d332895977e89c1bbc80fcee2 - languageName: node - linkType: hard - "define-properties@npm:^1.1.2, define-properties@npm:^1.2.1": version: 1.2.1 resolution: "define-properties@npm:1.2.1" @@ -8546,14 +8636,15 @@ __metadata: languageName: node linkType: hard -"eciesjs@npm:^0.3.15": - version: 0.3.19 - resolution: "eciesjs@npm:0.3.19" +"eciesjs@npm:^0.4.8": + version: 0.4.11 + resolution: "eciesjs@npm:0.4.11" dependencies: - "@types/secp256k1": "npm:^4.0.6" - futoin-hkdf: "npm:^1.5.3" - secp256k1: "npm:^5.0.0" - checksum: 10/35cdf409c39500662a62e9b6775f7ad65cbb598eb2a19ebf5d78e7444b995fe533f2a0ce97f90bb8787c32f3d5a9e35d5e9725536b515d3dd336a52a68b3791d + "@ecies/ciphers": "npm:^0.2.1" + "@noble/ciphers": "npm:^1.0.0" + "@noble/curves": "npm:^1.6.0" + "@noble/hashes": "npm:^1.5.0" + checksum: 10/3906d6286c4cde8dd93f5b8e1ad085aa0fdfd9a272c77a382062a782693247d19b6a99d749aff77d037777cfc49c02a8869a3aad47f192ac4f473b87cdbff4af languageName: node linkType: hard @@ -8623,21 +8714,6 @@ __metadata: languageName: node linkType: hard -"elliptic@npm:^6.5.4": - version: 6.5.5 - resolution: "elliptic@npm:6.5.5" - dependencies: - bn.js: "npm:^4.11.9" - brorand: "npm:^1.1.0" - hash.js: "npm:^1.0.0" - hmac-drbg: "npm:^1.0.1" - inherits: "npm:^2.0.4" - minimalistic-assert: "npm:^1.0.1" - minimalistic-crypto-utils: "npm:^1.0.1" - checksum: 10/5444b4f18e0c0fdfa14de26f69f7dbc44c78a211e91825823d698dcc91071ef1a3954d87730f364183fc83b0a86d8affed864e347da2e549bdcead3b46de126f - languageName: node - linkType: hard - "elliptic@npm:^6.5.7": version: 6.6.0 resolution: "elliptic@npm:6.6.0" @@ -9458,6 +9534,13 @@ __metadata: languageName: node linkType: hard +"expect-type@npm:^1.1.0": + version: 1.1.0 + resolution: "expect-type@npm:1.1.0" + checksum: 10/05fca80ddc7d493a89361f783c6b000750fa04a8226bc24701f3b90adb0efc2fb467f2a0baaed4015a02d8b9034ef5bb87521df9dba980f50b1105bd596ef833 + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -9952,13 +10035,6 @@ __metadata: languageName: node linkType: hard -"futoin-hkdf@npm:^1.5.3": - version: 1.5.3 - resolution: "futoin-hkdf@npm:1.5.3" - checksum: 10/aa64b93b4fdca77e6e9c7f045c539dd912f10077bc31d933e219eb5784e88e90a6d830b5d34431da840cc7477c0ed5f2d504dec49718b9f57941de5f23c20471 - languageName: node - linkType: hard - "gauge@npm:^4.0.3": version: 4.0.4 resolution: "gauge@npm:4.0.4" @@ -9991,7 +10067,7 @@ __metadata: languageName: node linkType: hard -"get-caller-file@npm:^2.0.1, get-caller-file@npm:^2.0.5": +"get-caller-file@npm:^2.0.1": version: 2.0.5 resolution: "get-caller-file@npm:2.0.5" checksum: 10/b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 @@ -10519,12 +10595,12 @@ __metadata: languageName: node linkType: hard -"i18next@npm:22.5.1": - version: 22.5.1 - resolution: "i18next@npm:22.5.1" +"i18next@npm:23.11.5": + version: 23.11.5 + resolution: "i18next@npm:23.11.5" dependencies: - "@babel/runtime": "npm:^7.20.6" - checksum: 10/ab1a0adee97911917fc46fb4216b8eb7c4ec0a243966609dda6a384e4b22acd25386a817dc51146328d5272ce1c6133558361788ebc4a36fbca250b8b3e90bd1 + "@babel/runtime": "npm:^7.23.2" + checksum: 10/3a8e0d5d2b9ac6c6fa8c2180452aaf816d60e1cc790da69d6be515feec85553f8af9fcc19414ade1a621f08236e84f38df4415a8234919fa97fa2e35624e86b6 languageName: node linkType: hard @@ -10810,15 +10886,6 @@ __metadata: languageName: node linkType: hard -"is-docker@npm:^2.0.0, is-docker@npm:^2.1.1": - version: 2.2.1 - resolution: "is-docker@npm:2.2.1" - bin: - is-docker: cli.js - checksum: 10/3fef7ddbf0be25958e8991ad941901bf5922ab2753c46980b60b05c1bf9c9c2402d35e6dc32e4380b980ef5e1970a5d9d5e5aa2e02d77727c3b6b5e918474c56 - languageName: node - linkType: hard - "is-docker@npm:^3.0.0": version: 3.0.0 resolution: "is-docker@npm:3.0.0" @@ -11091,15 +11158,6 @@ __metadata: languageName: node linkType: hard -"is-wsl@npm:^2.2.0": - version: 2.2.0 - resolution: "is-wsl@npm:2.2.0" - dependencies: - is-docker: "npm:^2.0.0" - checksum: 10/20849846ae414997d290b75e16868e5261e86ff5047f104027026fd61d8b5a9b0b3ade16239f35e1a067b3c7cc02f70183cb661010ed16f4b6c7c93dad1b19d8 - languageName: node - linkType: hard - "is-wsl@npm:^3.1.0": version: 3.1.0 resolution: "is-wsl@npm:3.1.0" @@ -11172,21 +11230,21 @@ __metadata: languageName: node linkType: hard -"isows@npm:1.0.3": - version: 1.0.3 - resolution: "isows@npm:1.0.3" +"isows@npm:1.0.4": + version: 1.0.4 + resolution: "isows@npm:1.0.4" peerDependencies: ws: "*" - checksum: 10/9cacd5cf59f67deb51e825580cd445ab1725ecb05a67c704050383fb772856f3cd5e7da8ad08f5a3bd2823680d77d099459d0c6a7037972a74d6429af61af440 + checksum: 10/a3ee62e3d6216abb3adeeb2a551fe2e7835eac87b05a6ecc3e7739259bf5f8e83290501f49e26137390c8093f207fc3378d4a7653aab76ad7bbab4b2dba9c5b9 languageName: node linkType: hard -"isows@npm:1.0.4": - version: 1.0.4 - resolution: "isows@npm:1.0.4" +"isows@npm:1.0.6": + version: 1.0.6 + resolution: "isows@npm:1.0.6" peerDependencies: ws: "*" - checksum: 10/a3ee62e3d6216abb3adeeb2a551fe2e7835eac87b05a6ecc3e7739259bf5f8e83290501f49e26137390c8093f207fc3378d4a7653aab76ad7bbab4b2dba9c5b9 + checksum: 10/ab9e85b50bcc3d70aa5ec875aa2746c5daf9321cb376ed4e5434d3c2643c5d62b1f466d93a05cd2ad0ead5297224922748c31707cb4fbd68f5d05d0479dce99c languageName: node linkType: hard @@ -11771,6 +11829,13 @@ __metadata: languageName: node linkType: hard +"loupe@npm:^3.1.2": + version: 3.1.2 + resolution: "loupe@npm:3.1.2" + checksum: 10/8f5734e53fb64cd914aa7d986e01b6d4c2e3c6c56dcbd5428d71c2703f0ab46b5ab9f9eeaaf2b485e8a1c43f865bdd16ec08ae1a661c8f55acdbd9f4d59c607a + languageName: node + linkType: hard + "lru-cache@npm:^10.2.0": version: 10.2.0 resolution: "lru-cache@npm:10.2.0" @@ -11871,6 +11936,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.12": + version: 0.30.12 + resolution: "magic-string@npm:0.30.12" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.0" + checksum: 10/98016180a52b28efc1362152b45671067facccdaead6b70c1c14c566cba98491bc2e1336474b0996397730dca24400e85649da84d3da62b2560ed03c067573e6 + languageName: node + linkType: hard + "make-error@npm:^1.1.1": version: 1.3.6 resolution: "make-error@npm:1.3.6" @@ -12204,17 +12278,15 @@ __metadata: languageName: node linkType: hard -"mipd@npm:0.0.5": - version: 0.0.5 - resolution: "mipd@npm:0.0.5" - dependencies: - viem: "npm:^1.1.4" +"mipd@npm:0.0.7": + version: 0.0.7 + resolution: "mipd@npm:0.0.7" peerDependencies: typescript: ">=5.0.4" peerDependenciesMeta: typescript: optional: true - checksum: 10/f4f0929b3a8dafa8480bc73ec34bfc61b4488861337022571cd02d076111b4afb202faade6d8b3f05f85403fcd76564d3aa758368d3cef14dae10ef6b52d7596 + checksum: 10/c14dffef0ef7a3e71469aee553f5735f4a6a9f9a2b47ca02798040f2e006261c2e7e8b26ee0dc56a815c04d5612eb4be1eed474e7bb4e496eb0f5ada2fe1d2e7 languageName: node linkType: hard @@ -12331,7 +12403,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:^2.0.0, ms@npm:^2.1.1": +"ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: 10/aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d @@ -12439,15 +12511,6 @@ __metadata: languageName: node linkType: hard -"node-addon-api@npm:^5.0.0": - version: 5.1.0 - resolution: "node-addon-api@npm:5.1.0" - dependencies: - node-gyp: "npm:latest" - checksum: 10/595f59ffb4630564f587c502119cbd980d302e482781021f3b479f5fc7e41cf8f2f7280fdc2795f32d148e4f3259bd15043c52d4a3442796aa6f1ae97b959636 - languageName: node - linkType: hard - "node-addon-api@npm:^7.0.0": version: 7.1.0 resolution: "node-addon-api@npm:7.1.0" @@ -12918,17 +12981,6 @@ __metadata: languageName: node linkType: hard -"open@npm:^8.4.0": - version: 8.4.2 - resolution: "open@npm:8.4.2" - dependencies: - define-lazy-prop: "npm:^2.0.0" - is-docker: "npm:^2.1.1" - is-wsl: "npm:^2.2.0" - checksum: 10/acd81a1d19879c818acb3af2d2e8e9d81d17b5367561e623248133deb7dd3aefaed527531df2677d3e6aaf0199f84df57b6b2262babff8bf46ea0029aac536c9 - languageName: node - linkType: hard - "optionator@npm:^0.9.1": version: 0.9.1 resolution: "optionator@npm:0.9.1" @@ -12950,6 +13002,46 @@ __metadata: languageName: node linkType: hard +"ox@npm:0.1.0": + version: 0.1.0 + resolution: "ox@npm:0.1.0" + dependencies: + "@adraffy/ens-normalize": "npm:^1.10.1" + "@noble/curves": "npm:^1.6.0" + "@noble/hashes": "npm:^1.5.0" + "@scure/bip32": "npm:^1.5.0" + "@scure/bip39": "npm:^1.4.0" + abitype: "npm:^1.0.6" + eventemitter3: "npm:5.0.1" + peerDependencies: + typescript: ">=5.4.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/2b98a729df12204c2b0f7f3c58e6e043db68544d040e101108c8d2421759be5f0729fd0fcedebc1b61ef30bac14229ff5e6d5514de9095dc9b9a2eec64d60b24 + languageName: node + linkType: hard + +"ox@npm:0.1.2": + version: 0.1.2 + resolution: "ox@npm:0.1.2" + dependencies: + "@adraffy/ens-normalize": "npm:^1.10.1" + "@noble/curves": "npm:^1.6.0" + "@noble/hashes": "npm:^1.5.0" + "@scure/bip32": "npm:^1.5.0" + "@scure/bip39": "npm:^1.4.0" + abitype: "npm:^1.0.6" + eventemitter3: "npm:5.0.1" + peerDependencies: + typescript: ">=5.4.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/cba00f13289599ff03cee3dbc19167c1d0f01829379d119f962b4e951ee2bf0d14491c7a45974e6a2a745117b13b22e9e4131d285e1f5247ea4e1cbc43c5c3d8 + languageName: node + linkType: hard + "p-finally@npm:^1.0.0": version: 1.0.0 resolution: "p-finally@npm:1.0.0" @@ -13864,6 +13956,13 @@ __metadata: languageName: node linkType: hard +"preact@npm:^10.24.2": + version: 10.24.3 + resolution: "preact@npm:10.24.3" + checksum: 10/e9c4c901a4ddd475a1072355b5c6c944b05797445e0d68f317ad0dbc976b831523573693ea75d2e12e7902042e3729af435377816d25558bf693ecf6b516c707 + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -14751,25 +14850,6 @@ __metadata: languageName: node linkType: hard -"rollup-plugin-visualizer@npm:^5.9.2": - version: 5.12.0 - resolution: "rollup-plugin-visualizer@npm:5.12.0" - dependencies: - open: "npm:^8.4.0" - picomatch: "npm:^2.3.1" - source-map: "npm:^0.7.4" - yargs: "npm:^17.5.1" - peerDependencies: - rollup: 2.x || 3.x || 4.x - peerDependenciesMeta: - rollup: - optional: true - bin: - rollup-plugin-visualizer: dist/bin/cli.js - checksum: 10/47358feb672291d6edcfd94197577c192a84c24cb644119425dae8241fb6f5a52556efd0c501f38b276c07534642a80c0885ef681babb474e83c7b5a3b475b84 - languageName: node - linkType: hard - "rollup-pluginutils@npm:^2.8.2": version: 2.8.2 resolution: "rollup-pluginutils@npm:2.8.2" @@ -14945,18 +15025,6 @@ __metadata: languageName: node linkType: hard -"secp256k1@npm:^5.0.0": - version: 5.0.0 - resolution: "secp256k1@npm:5.0.0" - dependencies: - elliptic: "npm:^6.5.4" - node-addon-api: "npm:^5.0.0" - node-gyp: "npm:latest" - node-gyp-build: "npm:^4.2.0" - checksum: 10/6e146c876ef202dbfbb35836d6ccd0ea3779dc09bad632bb9e0fe2e702848a4ee96638f39da54895430de832232d6292d858529e2eda56db3ddda13e40d7facc - languageName: node - linkType: hard - "semver@npm:^5.6.0": version: 5.7.2 resolution: "semver@npm:5.7.2" @@ -15904,6 +15972,20 @@ __metadata: languageName: node linkType: hard +"tinybench@npm:^2.9.0": + version: 2.9.0 + resolution: "tinybench@npm:2.9.0" + checksum: 10/cfa1e1418e91289219501703c4693c70708c91ffb7f040fd318d24aef419fb5a43e0c0160df9471499191968b2451d8da7f8087b08c3133c251c40d24aced06c + languageName: node + linkType: hard + +"tinyexec@npm:^0.3.1": + version: 0.3.1 + resolution: "tinyexec@npm:0.3.1" + checksum: 10/0537c70590d52d354f40c0255ff0f654a3d18ddb3812b440ddf9d436edf516c8057838ad5a38744c0c59670ec03e3cf23fbe04ae3d49f031d948274e99002569 + languageName: node + linkType: hard + "tinypool@npm:^1.0.0": version: 1.0.0 resolution: "tinypool@npm:1.0.0" @@ -15911,6 +15993,13 @@ __metadata: languageName: node linkType: hard +"tinypool@npm:^1.0.1": + version: 1.0.1 + resolution: "tinypool@npm:1.0.1" + checksum: 10/eaceb93784b8e27e60c0e3e2c7d11c29e1e79b2a025b2c232215db73b90fe22bd4753ad53fc8e801c2b5a63b94a823af549555d8361272bc98271de7dd4a9925 + languageName: node + linkType: hard + "tinyrainbow@npm:^1.2.0": version: 1.2.0 resolution: "tinyrainbow@npm:1.2.0" @@ -15925,6 +16014,13 @@ __metadata: languageName: node linkType: hard +"tinyspy@npm:^3.0.2": + version: 3.0.2 + resolution: "tinyspy@npm:3.0.2" + checksum: 10/5db671b2ff5cd309de650c8c4761ca945459d7204afb1776db9a04fb4efa28a75f08517a8620c01ee32a577748802231ad92f7d5b194dc003ee7f987a2a06337 + languageName: node + linkType: hard + "to-fast-properties@npm:^2.0.0": version: 2.0.0 resolution: "to-fast-properties@npm:2.0.0" @@ -16597,16 +16693,6 @@ __metadata: languageName: node linkType: hard -"utf-8-validate@npm:^6.0.3": - version: 6.0.4 - resolution: "utf-8-validate@npm:6.0.4" - dependencies: - node-gyp: "npm:latest" - node-gyp-build: "npm:^4.3.0" - checksum: 10/046fc81e7c7528661b8162262e62750ab23439e9901dece161b4c6faf36c699f55b8ebd3d9701da2881e315cdce1ee9597121f7dd2f4e405b8aeeec09ded8684 - languageName: node - linkType: hard - "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -16729,6 +16815,28 @@ __metadata: languageName: node linkType: hard +"viem@npm:2.21.43": + version: 2.21.43 + resolution: "viem@npm:2.21.43" + dependencies: + "@noble/curves": "npm:1.6.0" + "@noble/hashes": "npm:1.5.0" + "@scure/bip32": "npm:1.5.0" + "@scure/bip39": "npm:1.4.0" + abitype: "npm:1.0.6" + isows: "npm:1.0.6" + ox: "npm:0.1.0" + webauthn-p256: "npm:0.0.10" + ws: "npm:8.18.0" + peerDependencies: + typescript: ">=5.0.4" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/de765d56562eb4d68352dc8116236421281d6004be4b143d402f0cc1c97dc0d880133eea26a958f7a34c355d9253107b774a917a2a73391d0af22a45f0a4c3ff + languageName: node + linkType: hard + "viem@npm:2.x": version: 2.15.1 resolution: "viem@npm:2.15.1" @@ -16750,24 +16858,25 @@ __metadata: languageName: node linkType: hard -"viem@npm:^1.0.0, viem@npm:^1.1.4": - version: 1.21.4 - resolution: "viem@npm:1.21.4" +"viem@npm:^2.1.1": + version: 2.21.44 + resolution: "viem@npm:2.21.44" dependencies: - "@adraffy/ens-normalize": "npm:1.10.0" - "@noble/curves": "npm:1.2.0" - "@noble/hashes": "npm:1.3.2" - "@scure/bip32": "npm:1.3.2" - "@scure/bip39": "npm:1.2.1" - abitype: "npm:0.9.8" - isows: "npm:1.0.3" - ws: "npm:8.13.0" + "@noble/curves": "npm:1.6.0" + "@noble/hashes": "npm:1.5.0" + "@scure/bip32": "npm:1.5.0" + "@scure/bip39": "npm:1.4.0" + abitype: "npm:1.0.6" + isows: "npm:1.0.6" + ox: "npm:0.1.2" + webauthn-p256: "npm:0.0.10" + ws: "npm:8.18.0" peerDependencies: typescript: ">=5.0.4" peerDependenciesMeta: typescript: optional: true - checksum: 10/2007a8a674301d790b3172a0a84bd1659f76332ac13a78d695f7cee0602388103a07b2d6a3fc46b4f27582f8b506f7c1f90f13c5e21e464daffc6cccb14fbc3a + checksum: 10/c909480514128165f927d369c887b827191c9f104638866521ab2fce74ab9ef388a436bc781b661a5a93bb96f0d5e2bcde37e3b8ff7fdb3d36c387a783d9342e languageName: node linkType: hard @@ -16786,6 +16895,20 @@ __metadata: languageName: node linkType: hard +"vite-node@npm:2.1.4": + version: 2.1.4 + resolution: "vite-node@npm:2.1.4" + dependencies: + cac: "npm:^6.7.14" + debug: "npm:^4.3.7" + pathe: "npm:^1.1.2" + vite: "npm:^5.0.0" + bin: + vite-node: vite-node.mjs + checksum: 10/3c3fbe6e41ab1716f4e6e0b52dcb80e027cb481df03e31d9bb5d16bb0ffabc5c884cca705ef8a5dea60f787e5eb78a428977d0d40e61e1f331bfb8c3d486d3e2 + languageName: node + linkType: hard + "vite-plugin-node-polyfills@npm:^0.22.0": version: 0.22.0 resolution: "vite-plugin-node-polyfills@npm:0.22.0" @@ -16887,6 +17010,56 @@ __metadata: languageName: node linkType: hard +"vitest@npm:^2.1.2": + version: 2.1.4 + resolution: "vitest@npm:2.1.4" + dependencies: + "@vitest/expect": "npm:2.1.4" + "@vitest/mocker": "npm:2.1.4" + "@vitest/pretty-format": "npm:^2.1.4" + "@vitest/runner": "npm:2.1.4" + "@vitest/snapshot": "npm:2.1.4" + "@vitest/spy": "npm:2.1.4" + "@vitest/utils": "npm:2.1.4" + chai: "npm:^5.1.2" + debug: "npm:^4.3.7" + expect-type: "npm:^1.1.0" + magic-string: "npm:^0.30.12" + pathe: "npm:^1.1.2" + std-env: "npm:^3.7.0" + tinybench: "npm:^2.9.0" + tinyexec: "npm:^0.3.1" + tinypool: "npm:^1.0.1" + tinyrainbow: "npm:^1.2.0" + vite: "npm:^5.0.0" + vite-node: "npm:2.1.4" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@types/node": ^18.0.0 || >=20.0.0 + "@vitest/browser": 2.1.4 + "@vitest/ui": 2.1.4 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@types/node": + optional: true + "@vitest/browser": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: 10/bf0bb39e6148678ccc0d856a6a08e99458e80266558f97757bd20980812cd439f51599bcb64c807805594bf6fdb2111fdca688bc8884524819cc4a84a4598109 + languageName: node + linkType: hard + "vm-browserify@npm:^1.0.1": version: 1.1.2 resolution: "vm-browserify@npm:1.1.2" @@ -16969,22 +17142,22 @@ __metadata: tailwindcss: "npm:^3.4.3" ts-node: "npm:^10.9.1" typescript: "npm:^5.3.3" - viem: "npm:2.x" + viem: "npm:2.21.43" vite: "npm:^5.3.5" vite-plugin-node-polyfills: "npm:^0.22.0" vitest: "npm:^2.0.5" - wagmi: "npm:^2.10.3" + wagmi: "npm:^2.12.29" web3: "npm:^4.10.0" yup: "npm:^1.4.0" languageName: unknown linkType: soft -"wagmi@npm:^2.10.3": - version: 2.10.3 - resolution: "wagmi@npm:2.10.3" +"wagmi@npm:^2.12.29": + version: 2.12.30 + resolution: "wagmi@npm:2.12.30" dependencies: - "@wagmi/connectors": "npm:5.0.15" - "@wagmi/core": "npm:2.11.3" + "@wagmi/connectors": "npm:5.3.8" + "@wagmi/core": "npm:2.14.5" use-sync-external-store: "npm:1.2.0" peerDependencies: "@tanstack/react-query": ">=5.0.0" @@ -16994,7 +17167,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/8bec26c57d680ecd46c62f7cf84ef506b72b0e50705988759a95b27a07de592c5861a3c10f2f7e968078ece9aba3fc3e94c419e76d64acde520e3f3348830517 + checksum: 10/461a0640afc3b0868c17c2dbeb8768b7fe3d21b8392866da1cbb1af6233c8c64d3ba3ae5861a1167eb7f54bd62dbb116cd36bd2c50a1c5daefe5be13b44d83d1 languageName: node linkType: hard @@ -17269,6 +17442,16 @@ __metadata: languageName: node linkType: hard +"webauthn-p256@npm:0.0.10": + version: 0.0.10 + resolution: "webauthn-p256@npm:0.0.10" + dependencies: + "@noble/curves": "npm:^1.4.0" + "@noble/hashes": "npm:^1.4.0" + checksum: 10/dde2b6313b6a0f20996f7ee90181258fc7685bfff401df7d904578da75b374f25d5b9c1189cd2fcec30625b1f276b393188d156d49783f0611623cd713bb5b09 + languageName: node + linkType: hard + "webextension-polyfill@npm:>=0.10.0 <1.0": version: 0.12.0 resolution: "webextension-polyfill@npm:0.12.0" @@ -17440,7 +17623,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" dependencies: @@ -17500,9 +17683,9 @@ __metadata: languageName: node linkType: hard -"ws@npm:8.13.0, ws@npm:^8.8.1": - version: 8.13.0 - resolution: "ws@npm:8.13.0" +"ws@npm:8.17.1, ws@npm:~8.17.1": + version: 8.17.1 + resolution: "ws@npm:8.17.1" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -17511,13 +17694,13 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10/1769532b6fdab9ff659f0b17810e7501831d34ecca23fd179ee64091dd93a51f42c59f6c7bb4c7a384b6c229aca8076fb312aa35626257c18081511ef62a161d + checksum: 10/4264ae92c0b3e59c7e309001e93079b26937aab181835fb7af79f906b22cd33b6196d96556dafb4e985742dd401e99139572242e9847661fdbc96556b9e6902d languageName: node linkType: hard -"ws@npm:8.17.1, ws@npm:~8.17.1": - version: 8.17.1 - resolution: "ws@npm:8.17.1" +"ws@npm:8.18.0, ws@npm:^8.16.0": + version: 8.18.0 + resolution: "ws@npm:8.18.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -17526,7 +17709,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10/4264ae92c0b3e59c7e309001e93079b26937aab181835fb7af79f906b22cd33b6196d96556dafb4e985742dd401e99139572242e9847661fdbc96556b9e6902d + checksum: 10/70dfe53f23ff4368d46e4c0b1d4ca734db2c4149c6f68bc62cb16fc21f753c47b35fcc6e582f3bdfba0eaeb1c488cddab3c2255755a5c3eecb251431e42b3ff6 languageName: node linkType: hard @@ -17545,9 +17728,9 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.16.0": - version: 8.18.0 - resolution: "ws@npm:8.18.0" +"ws@npm:^8.8.1": + version: 8.13.0 + resolution: "ws@npm:8.13.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -17556,7 +17739,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10/70dfe53f23ff4368d46e4c0b1d4ca734db2c4149c6f68bc62cb16fc21f753c47b35fcc6e582f3bdfba0eaeb1c488cddab3c2255755a5c3eecb251431e42b3ff6 + checksum: 10/1769532b6fdab9ff659f0b17810e7501831d34ecca23fd179ee64091dd93a51f42c59f6c7bb4c7a384b6c229aca8076fb312aa35626257c18081511ef62a161d languageName: node linkType: hard @@ -17581,13 +17764,6 @@ __metadata: languageName: node linkType: hard -"y18n@npm:^5.0.5": - version: 5.0.8 - resolution: "y18n@npm:5.0.8" - checksum: 10/5f1b5f95e3775de4514edbb142398a2c37849ccfaf04a015be5d75521e9629d3be29bd4432d23c57f37e5b61ade592fb0197022e9993f81a06a5afbdcda9346d - languageName: node - linkType: hard - "yaeti@npm:^0.0.6": version: 0.0.6 resolution: "yaeti@npm:0.0.6" @@ -17642,13 +17818,6 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^21.1.1": - version: 21.1.1 - resolution: "yargs-parser@npm:21.1.1" - checksum: 10/9dc2c217ea3bf8d858041252d43e074f7166b53f3d010a8c711275e09cd3d62a002969a39858b92bbda2a6a63a585c7127014534a560b9c69ed2d923d113406e - languageName: node - linkType: hard - "yargs@npm:^15.3.1": version: 15.4.1 resolution: "yargs@npm:15.4.1" @@ -17668,21 +17837,6 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.5.1": - version: 17.7.2 - resolution: "yargs@npm:17.7.2" - dependencies: - cliui: "npm:^8.0.1" - escalade: "npm:^3.1.1" - get-caller-file: "npm:^2.0.5" - require-directory: "npm:^2.1.1" - string-width: "npm:^4.2.3" - y18n: "npm:^5.0.5" - yargs-parser: "npm:^21.1.1" - checksum: 10/abb3e37678d6e38ea85485ed86ebe0d1e3464c640d7d9069805ea0da12f69d5a32df8e5625e370f9c96dd1c2dc088ab2d0a4dd32af18222ef3c4224a19471576 - languageName: node - linkType: hard - "yn@npm:3.1.1": version: 3.1.1 resolution: "yn@npm:3.1.1" @@ -17723,15 +17877,14 @@ __metadata: languageName: node linkType: hard -"zustand@npm:4.4.1": - version: 4.4.1 - resolution: "zustand@npm:4.4.1" - dependencies: - use-sync-external-store: "npm:1.2.0" +"zustand@npm:5.0.0": + version: 5.0.0 + resolution: "zustand@npm:5.0.0" peerDependencies: - "@types/react": ">=16.8" - immer: ">=9.0" - react: ">=16.8" + "@types/react": ">=18.0.0" + immer: ">=9.0.6" + react: ">=18.0.0" + use-sync-external-store: ">=1.2.0" peerDependenciesMeta: "@types/react": optional: true @@ -17739,6 +17892,8 @@ __metadata: optional: true react: optional: true - checksum: 10/e6e21cbb7200bd9eca35c8f385d8b4c06949581f4e19a11c473fe2df5b756997e7d4747eb9f54ee918b9a378c62e3f2f6eadba9d24f9eb4351cc50ad27832c13 + use-sync-external-store: + optional: true + checksum: 10/be75ef4d1b218b143314467bb9e23641231043cad2d5c3a4b2219c46d1609ee799cd8dc9acec9b23d55ec3a2a619a06616e593aea4049f3b7323938af9a33bfe languageName: node linkType: hard From a26db92477b2f30e69b7961e00f1d2c0570b4a02 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 12 Nov 2024 19:15:59 +0100 Subject: [PATCH 022/221] Change wagmiConfig.ts --- src/wagmiConfig.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wagmiConfig.ts b/src/wagmiConfig.ts index 5a496a93..2d3b14e1 100644 --- a/src/wagmiConfig.ts +++ b/src/wagmiConfig.ts @@ -30,6 +30,7 @@ const wagmiAdapter = new WagmiAdapter({ networks, projectId, ssr: false, + transports, }); // 5. Create modal @@ -44,7 +45,8 @@ createAppKit({ socials: false, swaps: false, }, - featuredWalletIds: ['c57ca95b47569778a828d19178114f4db188b89b763c899ba0be274e97267d96'], // metamask + // metamask is somehow not always included + featuredWalletIds: ['c57ca95b47569778a828d19178114f4db188b89b763c899ba0be274e97267d96'], metadata: undefined, // Optional }); From acec761852c900f45137043197c606dd1e76723b Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 12 Nov 2024 19:16:07 +0100 Subject: [PATCH 023/221] Remove dependencies --- package.json | 7 +- yarn.lock | 537 +++------------------------------------------------ 2 files changed, 32 insertions(+), 512 deletions(-) diff --git a/package.json b/package.json index a6e63769..e4b6ddda 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "@polkadot/types": "^13.2.1", "@polkadot/util": "^13.1.1", "@polkadot/util-crypto": "^13.1.1", - "@rainbow-me/rainbowkit": "^2.1.7", "@reown/appkit": "^1.3.1", "@reown/appkit-adapter-wagmi": "^1.3.1", "@sentry/react": "^8.36.0", @@ -42,8 +41,6 @@ "@talismn/connect-components": "^1.1.8", "@talismn/connect-wallets": "^1.2.5", "@tanstack/react-query": "^5.45.1", - "@walletconnect/modal": "^2.6.2", - "@walletconnect/universal-provider": "^2.12.2", "autoprefixer": "^10.4.19", "big.js": "^6.2.1", "bn.js": "^5.2.1", @@ -62,8 +59,8 @@ "stellar-sdk": "^11.3.0", "tailwind": "^4.0.0", "tailwindcss": "^3.4.3", - "viem": "2.21.43", - "wagmi": "^2.12.29", + "viem": "^2.21.44", + "wagmi": "^2.12.30", "web3": "^4.10.0", "yup": "^1.4.0" }, diff --git a/yarn.lock b/yarn.lock index 4212640b..8fd09dc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1868,13 +1868,6 @@ __metadata: languageName: node linkType: hard -"@emotion/hash@npm:^0.9.0": - version: 0.9.1 - resolution: "@emotion/hash@npm:0.9.1" - checksum: 10/716e17e48bf9047bf9383982c071de49f2615310fb4e986738931776f5a823bc1f29c84501abe0d3df91a3803c80122d24e28b57351bca9e01356ebb33d89876 - languageName: node - linkType: hard - "@esbuild/aix-ppc64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/aix-ppc64@npm:0.21.5" @@ -4026,27 +4019,6 @@ __metadata: languageName: node linkType: hard -"@rainbow-me/rainbowkit@npm:^2.1.7": - version: 2.1.7 - resolution: "@rainbow-me/rainbowkit@npm:2.1.7" - dependencies: - "@vanilla-extract/css": "npm:1.15.5" - "@vanilla-extract/dynamic": "npm:2.1.2" - "@vanilla-extract/sprinkles": "npm:1.6.3" - clsx: "npm:2.1.1" - qrcode: "npm:1.5.4" - react-remove-scroll: "npm:2.6.0" - ua-parser-js: "npm:^1.0.37" - peerDependencies: - "@tanstack/react-query": ">=5.0.0" - react: ">=18" - react-dom: ">=18" - viem: 2.x - wagmi: ^2.9.0 - checksum: 10/759187003cb33173bba01cafd5205c59df9a3dcc30a454dfc2a9234a35820fcf716ec054ff88abb294a39d4f3419cfd6f73a52c03a018ccc458567a6e33910ad - languageName: node - linkType: hard - "@remix-run/router@npm:1.6.1": version: 1.6.1 resolution: "@remix-run/router@npm:1.6.1" @@ -4865,7 +4837,7 @@ __metadata: languageName: node linkType: hard -"@stablelib/x25519@npm:1.0.3, @stablelib/x25519@npm:^1.0.3": +"@stablelib/x25519@npm:1.0.3": version: 1.0.3 resolution: "@stablelib/x25519@npm:1.0.3" dependencies: @@ -5471,51 +5443,6 @@ __metadata: languageName: node linkType: hard -"@vanilla-extract/css@npm:1.15.5": - version: 1.15.5 - resolution: "@vanilla-extract/css@npm:1.15.5" - dependencies: - "@emotion/hash": "npm:^0.9.0" - "@vanilla-extract/private": "npm:^1.0.6" - css-what: "npm:^6.1.0" - cssesc: "npm:^3.0.0" - csstype: "npm:^3.0.7" - dedent: "npm:^1.5.3" - deep-object-diff: "npm:^1.1.9" - deepmerge: "npm:^4.2.2" - lru-cache: "npm:^10.4.3" - media-query-parser: "npm:^2.0.2" - modern-ahocorasick: "npm:^1.0.0" - picocolors: "npm:^1.0.0" - checksum: 10/4820caea8f7d63d5e691c72d3d324a09707040afa6b0abaaf0fea7d9ee1c133a19e5f3a383fd903453680cd0d698de0428ad2a7316e0c5e9771ffd79d813ddf6 - languageName: node - linkType: hard - -"@vanilla-extract/dynamic@npm:2.1.2": - version: 2.1.2 - resolution: "@vanilla-extract/dynamic@npm:2.1.2" - dependencies: - "@vanilla-extract/private": "npm:^1.0.6" - checksum: 10/576b22e3f1a61abad2bc758d95f6f9eae9418f1bb6c8366211e82da7eed97ac8cf9b69fea3239832ccba280dab93d5b1def4290f64943b295f146fca78049d2d - languageName: node - linkType: hard - -"@vanilla-extract/private@npm:^1.0.6": - version: 1.0.6 - resolution: "@vanilla-extract/private@npm:1.0.6" - checksum: 10/50463610da0fc9069b3e2b33b6222ea2f005487432db9110ea430e474e29b3b756bcd1fffd47b87536358829d47bce6510398f050b5f6de07ee1e4e92eeade5a - languageName: node - linkType: hard - -"@vanilla-extract/sprinkles@npm:1.6.3": - version: 1.6.3 - resolution: "@vanilla-extract/sprinkles@npm:1.6.3" - peerDependencies: - "@vanilla-extract/css": ^1.0.0 - checksum: 10/74f8e2b189c0d48e279f1b85b5fedebf1f615ab31839964cf3861f2c5cf574567c0caeddf9c8b11327d81213f81d195efc79f136b725e6013b6d645d238d5c2b - languageName: node - linkType: hard - "@vitest/expect@npm:2.0.5": version: 2.0.5 resolution: "@vitest/expect@npm:2.0.5" @@ -5701,31 +5628,6 @@ __metadata: languageName: node linkType: hard -"@walletconnect/core@npm:2.12.2": - version: 2.12.2 - resolution: "@walletconnect/core@npm:2.12.2" - dependencies: - "@walletconnect/heartbeat": "npm:1.2.1" - "@walletconnect/jsonrpc-provider": "npm:1.0.13" - "@walletconnect/jsonrpc-types": "npm:1.0.3" - "@walletconnect/jsonrpc-utils": "npm:1.0.8" - "@walletconnect/jsonrpc-ws-connection": "npm:1.0.14" - "@walletconnect/keyvaluestorage": "npm:^1.1.1" - "@walletconnect/logger": "npm:^2.1.2" - "@walletconnect/relay-api": "npm:^1.0.9" - "@walletconnect/relay-auth": "npm:^1.0.4" - "@walletconnect/safe-json": "npm:^1.0.2" - "@walletconnect/time": "npm:^1.0.2" - "@walletconnect/types": "npm:2.12.2" - "@walletconnect/utils": "npm:2.12.2" - events: "npm:^3.3.0" - isomorphic-unfetch: "npm:3.1.0" - lodash.isequal: "npm:4.5.0" - uint8arrays: "npm:^3.1.0" - checksum: 10/640b6ef23115d98c9d7cc465a7a09c96bb36384fca9d4dafdc8d1e6b82ae05317aae84d0ebd22175d40d52a44beaa6f98458d3b7822e247b672dd3e77c576489 - languageName: node - linkType: hard - "@walletconnect/core@npm:2.17.0": version: 2.17.0 resolution: "@walletconnect/core@npm:2.17.0" @@ -5787,17 +5689,6 @@ __metadata: languageName: node linkType: hard -"@walletconnect/heartbeat@npm:1.2.1": - version: 1.2.1 - resolution: "@walletconnect/heartbeat@npm:1.2.1" - dependencies: - "@walletconnect/events": "npm:^1.0.1" - "@walletconnect/time": "npm:^1.0.2" - tslib: "npm:1.14.1" - checksum: 10/a68d7efe4e69c9749dd7c3a9e351dd22adccbb925447dd7f2b2978a4cd730695cc0b4e717a08bad0d0c60e0177b77618a53f3bfb4347659f3ccfe72d412c27fb - languageName: node - linkType: hard - "@walletconnect/heartbeat@npm:1.2.2": version: 1.2.2 resolution: "@walletconnect/heartbeat@npm:1.2.2" @@ -5821,29 +5712,6 @@ __metadata: languageName: node linkType: hard -"@walletconnect/jsonrpc-http-connection@npm:^1.0.7": - version: 1.0.7 - resolution: "@walletconnect/jsonrpc-http-connection@npm:1.0.7" - dependencies: - "@walletconnect/jsonrpc-utils": "npm:^1.0.6" - "@walletconnect/safe-json": "npm:^1.0.1" - cross-fetch: "npm:^3.1.4" - tslib: "npm:1.14.1" - checksum: 10/2d915df34e37592bdc69712244fd4e19da68eab42a8c576dd94cbca66ccdf30d4bf223c093042c0c5b9c8acb0e0af5cd682e8d9916098bd6cdea9593b9474971 - languageName: node - linkType: hard - -"@walletconnect/jsonrpc-provider@npm:1.0.13": - version: 1.0.13 - resolution: "@walletconnect/jsonrpc-provider@npm:1.0.13" - dependencies: - "@walletconnect/jsonrpc-utils": "npm:^1.0.8" - "@walletconnect/safe-json": "npm:^1.0.2" - tslib: "npm:1.14.1" - checksum: 10/27c7dfa898896ffd7250aecaf92b889663abe64ea605dae1b638743a9f1609f0e27b2bca761b3bbc2ed722bde1b012d901bba4de4067424905bfce514cc5e909 - languageName: node - linkType: hard - "@walletconnect/jsonrpc-provider@npm:1.0.14": version: 1.0.14 resolution: "@walletconnect/jsonrpc-provider@npm:1.0.14" @@ -5855,16 +5723,6 @@ __metadata: languageName: node linkType: hard -"@walletconnect/jsonrpc-types@npm:1.0.3, @walletconnect/jsonrpc-types@npm:^1.0.2, @walletconnect/jsonrpc-types@npm:^1.0.3": - version: 1.0.3 - resolution: "@walletconnect/jsonrpc-types@npm:1.0.3" - dependencies: - keyvaluestorage-interface: "npm:^1.0.0" - tslib: "npm:1.14.1" - checksum: 10/7b1209c2e6ff476e45b0d828bd4d7773873c4cff41e5ed235ff8014b4e8ff09ec704817347702fe3b8ca1c1b7920abfd0af94e0cdf582a92d8a0192d8c42dce8 - languageName: node - linkType: hard - "@walletconnect/jsonrpc-types@npm:1.0.4": version: 1.0.4 resolution: "@walletconnect/jsonrpc-types@npm:1.0.4" @@ -5875,7 +5733,17 @@ __metadata: languageName: node linkType: hard -"@walletconnect/jsonrpc-utils@npm:1.0.8, @walletconnect/jsonrpc-utils@npm:^1.0.6, @walletconnect/jsonrpc-utils@npm:^1.0.7, @walletconnect/jsonrpc-utils@npm:^1.0.8": +"@walletconnect/jsonrpc-types@npm:^1.0.2, @walletconnect/jsonrpc-types@npm:^1.0.3": + version: 1.0.3 + resolution: "@walletconnect/jsonrpc-types@npm:1.0.3" + dependencies: + keyvaluestorage-interface: "npm:^1.0.0" + tslib: "npm:1.14.1" + checksum: 10/7b1209c2e6ff476e45b0d828bd4d7773873c4cff41e5ed235ff8014b4e8ff09ec704817347702fe3b8ca1c1b7920abfd0af94e0cdf582a92d8a0192d8c42dce8 + languageName: node + linkType: hard + +"@walletconnect/jsonrpc-utils@npm:1.0.8, @walletconnect/jsonrpc-utils@npm:^1.0.6, @walletconnect/jsonrpc-utils@npm:^1.0.8": version: 1.0.8 resolution: "@walletconnect/jsonrpc-utils@npm:1.0.8" dependencies: @@ -5898,7 +5766,7 @@ __metadata: languageName: node linkType: hard -"@walletconnect/keyvaluestorage@npm:1.1.1, @walletconnect/keyvaluestorage@npm:^1.1.1": +"@walletconnect/keyvaluestorage@npm:1.1.1": version: 1.1.1 resolution: "@walletconnect/keyvaluestorage@npm:1.1.1" dependencies: @@ -5914,7 +5782,7 @@ __metadata: languageName: node linkType: hard -"@walletconnect/logger@npm:2.1.2, @walletconnect/logger@npm:^2.0.1, @walletconnect/logger@npm:^2.1.2": +"@walletconnect/logger@npm:2.1.2": version: 2.1.2 resolution: "@walletconnect/logger@npm:2.1.2" dependencies: @@ -5924,15 +5792,6 @@ __metadata: languageName: node linkType: hard -"@walletconnect/modal-core@npm:2.6.2": - version: 2.6.2 - resolution: "@walletconnect/modal-core@npm:2.6.2" - dependencies: - valtio: "npm:1.11.2" - checksum: 10/671184da341eebb6b7a3ad7c334851113683d71e6118f7203a377e493b61eb94bc0571484e497e577b9f4d7221a8a7034ad4b52af722c89fa4105627bed638ba - languageName: node - linkType: hard - "@walletconnect/modal-core@npm:2.7.0": version: 2.7.0 resolution: "@walletconnect/modal-core@npm:2.7.0" @@ -5942,18 +5801,6 @@ __metadata: languageName: node linkType: hard -"@walletconnect/modal-ui@npm:2.6.2": - version: 2.6.2 - resolution: "@walletconnect/modal-ui@npm:2.6.2" - dependencies: - "@walletconnect/modal-core": "npm:2.6.2" - lit: "npm:2.8.0" - motion: "npm:10.16.2" - qrcode: "npm:1.5.3" - checksum: 10/5460ad7f4591c016b723b3f707ac0020e185b60744cf7132b4b4f48d71c87c1c55826f6e11005860f96bd11e0ed3f88da7cda4c0a1c35a0e5b7d6e53bc14cf15 - languageName: node - linkType: hard - "@walletconnect/modal-ui@npm:2.7.0": version: 2.7.0 resolution: "@walletconnect/modal-ui@npm:2.7.0" @@ -5976,16 +5823,6 @@ __metadata: languageName: node linkType: hard -"@walletconnect/modal@npm:^2.6.2": - version: 2.6.2 - resolution: "@walletconnect/modal@npm:2.6.2" - dependencies: - "@walletconnect/modal-core": "npm:2.6.2" - "@walletconnect/modal-ui": "npm:2.6.2" - checksum: 10/f8f132c89d1d7f44f2fa564c8d5122163610be4afb0cadc9576c77083471297c37ff62aae3a25492c0ddb480240a2a6ffefe3eba1fd48f1664160c6bac01466d - languageName: node - linkType: hard - "@walletconnect/relay-api@npm:1.0.11": version: 1.0.11 resolution: "@walletconnect/relay-api@npm:1.0.11" @@ -5995,16 +5832,7 @@ __metadata: languageName: node linkType: hard -"@walletconnect/relay-api@npm:^1.0.9": - version: 1.0.10 - resolution: "@walletconnect/relay-api@npm:1.0.10" - dependencies: - "@walletconnect/jsonrpc-types": "npm:^1.0.2" - checksum: 10/0faeaed5bcd71da9f6b622d9d2cf2db3019108c61512032895e9bd9267a9f93edb7232489813df0a2770a88b83b2ebf8cf13159580f9126b81ebc283caebd4c6 - languageName: node - linkType: hard - -"@walletconnect/relay-auth@npm:1.0.4, @walletconnect/relay-auth@npm:^1.0.4": +"@walletconnect/relay-auth@npm:1.0.4": version: 1.0.4 resolution: "@walletconnect/relay-auth@npm:1.0.4" dependencies: @@ -6027,23 +5855,6 @@ __metadata: languageName: node linkType: hard -"@walletconnect/sign-client@npm:2.12.2": - version: 2.12.2 - resolution: "@walletconnect/sign-client@npm:2.12.2" - dependencies: - "@walletconnect/core": "npm:2.12.2" - "@walletconnect/events": "npm:^1.0.1" - "@walletconnect/heartbeat": "npm:1.2.1" - "@walletconnect/jsonrpc-utils": "npm:1.0.8" - "@walletconnect/logger": "npm:^2.1.2" - "@walletconnect/time": "npm:^1.0.2" - "@walletconnect/types": "npm:2.12.2" - "@walletconnect/utils": "npm:2.12.2" - events: "npm:^3.3.0" - checksum: 10/1c123659b304e80694c2ffa3a151ca66d1b06791f9741217ad3e9f9acb83aa591052f68a3202f5f2c50f5ca816b5d1e6a736bd1a8957fe97cb2118e8ae4c7fa3 - languageName: node - linkType: hard - "@walletconnect/sign-client@npm:2.17.0": version: 2.17.0 resolution: "@walletconnect/sign-client@npm:2.17.0" @@ -6070,20 +5881,6 @@ __metadata: languageName: node linkType: hard -"@walletconnect/types@npm:2.12.2": - version: 2.12.2 - resolution: "@walletconnect/types@npm:2.12.2" - dependencies: - "@walletconnect/events": "npm:^1.0.1" - "@walletconnect/heartbeat": "npm:1.2.1" - "@walletconnect/jsonrpc-types": "npm:1.0.3" - "@walletconnect/keyvaluestorage": "npm:^1.1.1" - "@walletconnect/logger": "npm:^2.0.1" - events: "npm:^3.3.0" - checksum: 10/7e90d57dbbb5998141953f9887b1cd591a486eaf98b344718f206c479c5d20aa8c6133c893b770b8a0e21649f7dcd8aa373ed82621b70a060df44e0c119cd2a3 - languageName: node - linkType: hard - "@walletconnect/types@npm:2.17.0": version: 2.17.0 resolution: "@walletconnect/types@npm:2.17.0" @@ -6115,45 +5912,6 @@ __metadata: languageName: node linkType: hard -"@walletconnect/universal-provider@npm:^2.12.2": - version: 2.12.2 - resolution: "@walletconnect/universal-provider@npm:2.12.2" - dependencies: - "@walletconnect/jsonrpc-http-connection": "npm:^1.0.7" - "@walletconnect/jsonrpc-provider": "npm:1.0.13" - "@walletconnect/jsonrpc-types": "npm:^1.0.2" - "@walletconnect/jsonrpc-utils": "npm:^1.0.7" - "@walletconnect/logger": "npm:^2.1.2" - "@walletconnect/sign-client": "npm:2.12.2" - "@walletconnect/types": "npm:2.12.2" - "@walletconnect/utils": "npm:2.12.2" - events: "npm:^3.3.0" - checksum: 10/c30a245b24f3fb0a155bf8493bf252f973f279be9b4294550d3deb08f57b354064f7594afe3daa3fec552176a7d346b31230b9e32279b76b27d9f0e9994881bb - languageName: node - linkType: hard - -"@walletconnect/utils@npm:2.12.2": - version: 2.12.2 - resolution: "@walletconnect/utils@npm:2.12.2" - dependencies: - "@stablelib/chacha20poly1305": "npm:1.0.1" - "@stablelib/hkdf": "npm:1.0.1" - "@stablelib/random": "npm:^1.0.2" - "@stablelib/sha256": "npm:1.0.1" - "@stablelib/x25519": "npm:^1.0.3" - "@walletconnect/relay-api": "npm:^1.0.9" - "@walletconnect/safe-json": "npm:^1.0.2" - "@walletconnect/time": "npm:^1.0.2" - "@walletconnect/types": "npm:2.12.2" - "@walletconnect/window-getters": "npm:^1.0.1" - "@walletconnect/window-metadata": "npm:^1.0.1" - detect-browser: "npm:5.3.0" - query-string: "npm:7.1.3" - uint8arrays: "npm:^3.1.0" - checksum: 10/088d7623ba39c10239f47a9fe30704daaf2569534c2d5bf3c8f5827a01f7ace4d0e5da9f22b8e5ec520abfa5fafebfa0112ec74994f96e5d98c7cfa373d4b02a - languageName: node - linkType: hard - "@walletconnect/utils@npm:2.17.0": version: 2.17.0 resolution: "@walletconnect/utils@npm:2.17.0" @@ -6187,7 +5945,7 @@ __metadata: languageName: node linkType: hard -"@walletconnect/window-metadata@npm:1.0.1, @walletconnect/window-metadata@npm:^1.0.1": +"@walletconnect/window-metadata@npm:1.0.1": version: 1.0.1 resolution: "@walletconnect/window-metadata@npm:1.0.1" dependencies: @@ -7523,13 +7281,6 @@ __metadata: languageName: node linkType: hard -"clsx@npm:2.1.1, clsx@npm:^2.1.0": - version: 2.1.1 - resolution: "clsx@npm:2.1.1" - checksum: 10/cdfb57fa6c7649bbff98d9028c2f0de2f91c86f551179541cf784b1cfdc1562dcb951955f46d54d930a3879931a980e32a46b598acaea274728dbe068deca919 - languageName: node - linkType: hard - "clsx@npm:^1.2.1": version: 1.2.1 resolution: "clsx@npm:1.2.1" @@ -7537,6 +7288,13 @@ __metadata: languageName: node linkType: hard +"clsx@npm:^2.1.0": + version: 2.1.1 + resolution: "clsx@npm:2.1.1" + checksum: 10/cdfb57fa6c7649bbff98d9028c2f0de2f91c86f551179541cf784b1cfdc1562dcb951955f46d54d930a3879931a980e32a46b598acaea274728dbe068deca919 + languageName: node + linkType: hard + "color-convert@npm:^1.9.0": version: 1.9.3 resolution: "color-convert@npm:1.9.3" @@ -8089,13 +7847,6 @@ __metadata: languageName: node linkType: hard -"csstype@npm:^3.0.7": - version: 3.1.3 - resolution: "csstype@npm:3.1.3" - checksum: 10/f593cce41ff5ade23f44e77521e3a1bcc2c64107041e1bf6c3c32adc5187d0d60983292fda326154d20b01079e24931aa5b08e4467cc488b60bb1e7f6d478ade - languageName: node - linkType: hard - "culori@npm:^3": version: 3.3.0 resolution: "culori@npm:3.3.0" @@ -8263,18 +8014,6 @@ __metadata: languageName: node linkType: hard -"dedent@npm:^1.5.3": - version: 1.5.3 - resolution: "dedent@npm:1.5.3" - peerDependencies: - babel-plugin-macros: ^3.1.0 - peerDependenciesMeta: - babel-plugin-macros: - optional: true - checksum: 10/e5277f6268f288649503125b781a7b7a2c9b22d011139688c0b3619fe40121e600eb1f077c891938d4b2428bdb6326cc3c77a763e4b1cc681bd9666ab1bad2a1 - languageName: node - linkType: hard - "deep-eql@npm:^5.0.1": version: 5.0.2 resolution: "deep-eql@npm:5.0.2" @@ -8315,20 +8054,6 @@ __metadata: languageName: node linkType: hard -"deep-object-diff@npm:^1.1.9": - version: 1.1.9 - resolution: "deep-object-diff@npm:1.1.9" - checksum: 10/b9771cc1ca08a34e408309eaab967bd2ab697684abdfa1262f4283ced8230a9ace966322f356364ff71a785c6e9cc356b7596582e900da5726e6b87d4b2a1463 - languageName: node - linkType: hard - -"deepmerge@npm:^4.2.2": - version: 4.3.1 - resolution: "deepmerge@npm:4.3.1" - checksum: 10/058d9e1b0ff1a154468bf3837aea436abcfea1ba1d165ddaaf48ca93765fdd01a30d33c36173da8fbbed951dd0a267602bc782fe288b0fc4b7e1e7091afc4529 - languageName: node - linkType: hard - "define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4": version: 1.1.4 resolution: "define-data-property@npm:1.1.4" @@ -8436,13 +8161,6 @@ __metadata: languageName: node linkType: hard -"detect-node-es@npm:^1.1.0": - version: 1.1.0 - resolution: "detect-node-es@npm:1.1.0" - checksum: 10/e46307d7264644975b71c104b9f028ed1d3d34b83a15b8a22373640ce5ea630e5640b1078b8ea15f202b54641da71e4aa7597093bd4b91f113db520a26a37449 - languageName: node - linkType: hard - "didyoumean@npm:^1.2.2": version: 1.2.2 resolution: "didyoumean@npm:1.2.2" @@ -10112,13 +9830,6 @@ __metadata: languageName: node linkType: hard -"get-nonce@npm:^1.0.0": - version: 1.0.1 - resolution: "get-nonce@npm:1.0.1" - checksum: 10/ad5104871d114a694ecc506a2d406e2331beccb961fe1e110dc25556b38bcdbf399a823a8a375976cd8889668156a9561e12ebe3fa6a4c6ba169c8466c2ff868 - languageName: node - linkType: hard - "get-own-enumerable-property-symbols@npm:^3.0.0": version: 3.0.2 resolution: "get-own-enumerable-property-symbols@npm:3.0.2" @@ -10747,7 +10458,7 @@ __metadata: languageName: node linkType: hard -"invariant@npm:2.2.4, invariant@npm:^2.2.4": +"invariant@npm:2.2.4": version: 2.2.4 resolution: "invariant@npm:2.2.4" dependencies: @@ -11211,16 +10922,6 @@ __metadata: languageName: node linkType: hard -"isomorphic-unfetch@npm:3.1.0": - version: 3.1.0 - resolution: "isomorphic-unfetch@npm:3.1.0" - dependencies: - node-fetch: "npm:^2.6.1" - unfetch: "npm:^4.2.0" - checksum: 10/4e760d9a3f94b42c59fe5c6b53202469cecd864875dcac927668b1f43eb57698422a0086fadde47f7815752c4f4e30ecf1ce9a0eb09c44a871a2484dbc580b39 - languageName: node - linkType: hard - "isomorphic-ws@npm:^5.0.0": version: 5.0.0 resolution: "isomorphic-ws@npm:5.0.0" @@ -11843,13 +11544,6 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^10.4.3": - version: 10.4.3 - resolution: "lru-cache@npm:10.4.3" - checksum: 10/e6e90267360476720fa8e83cc168aa2bf0311f3f2eea20a6ba78b90a885ae72071d9db132f40fda4129c803e7dcec3a6b6a6fbb44ca90b081630b810b5d6a41a - languageName: node - linkType: hard - "lru-cache@npm:^5.1.1": version: 5.1.1 resolution: "lru-cache@npm:5.1.1" @@ -11993,15 +11687,6 @@ __metadata: languageName: node linkType: hard -"media-query-parser@npm:^2.0.2": - version: 2.0.2 - resolution: "media-query-parser@npm:2.0.2" - dependencies: - "@babel/runtime": "npm:^7.12.5" - checksum: 10/9dff3ed135149944717a8687567f4fda1d39d28637f265c6ce7efe5ed55cd88ed49136c912ee0c7f3a6e5debc50b1ff969db609d862318f1af97f48752b08b0b - languageName: node - linkType: hard - "media-typer@npm:0.3.0": version: 0.3.0 resolution: "media-typer@npm:0.3.0" @@ -12341,13 +12026,6 @@ __metadata: languageName: node linkType: hard -"modern-ahocorasick@npm:^1.0.0": - version: 1.0.1 - resolution: "modern-ahocorasick@npm:1.0.1" - checksum: 10/ec83479f406511f37a966d66ce1c2b1701bb4a2cc2aabbbc257001178c9fbc48ce748c88eb10dfe72ba8b7f991a0bc7f1fa14683f444685edd1a9eeb32ecbc1e - languageName: node - linkType: hard - "moment@npm:2.22.2": version: 2.22.2 resolution: "moment@npm:2.22.2" @@ -12534,7 +12212,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.7": +"node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.7": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -13002,26 +12680,6 @@ __metadata: languageName: node linkType: hard -"ox@npm:0.1.0": - version: 0.1.0 - resolution: "ox@npm:0.1.0" - dependencies: - "@adraffy/ens-normalize": "npm:^1.10.1" - "@noble/curves": "npm:^1.6.0" - "@noble/hashes": "npm:^1.5.0" - "@scure/bip32": "npm:^1.5.0" - "@scure/bip39": "npm:^1.4.0" - abitype: "npm:^1.0.6" - eventemitter3: "npm:5.0.1" - peerDependencies: - typescript: ">=5.4.0" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/2b98a729df12204c2b0f7f3c58e6e043db68544d040e101108c8d2421759be5f0729fd0fcedebc1b61ef30bac14229ff5e6d5514de9095dc9b9a2eec64d60b24 - languageName: node - linkType: hard - "ox@npm:0.1.2": version: 0.1.2 resolution: "ox@npm:0.1.2" @@ -14170,19 +13828,6 @@ __metadata: languageName: node linkType: hard -"qrcode@npm:1.5.4": - version: 1.5.4 - resolution: "qrcode@npm:1.5.4" - dependencies: - dijkstrajs: "npm:^1.0.1" - pngjs: "npm:^5.0.0" - yargs: "npm:^15.3.1" - bin: - qrcode: bin/qrcode - checksum: 10/9a1b61760e4ea334545a0f54bbc11c537aba0a17cf52cab9fa1b07f8a1337eed0bc6f7fde41b197f2c82c249bc48728983bfaf861bb7ecb29dc597b2ae33c424 - languageName: node - linkType: hard - "qs@npm:6.5.2": version: 6.5.2 resolution: "qs@npm:6.5.2" @@ -14346,41 +13991,6 @@ __metadata: languageName: node linkType: hard -"react-remove-scroll-bar@npm:^2.3.6": - version: 2.3.6 - resolution: "react-remove-scroll-bar@npm:2.3.6" - dependencies: - react-style-singleton: "npm:^2.2.1" - tslib: "npm:^2.0.0" - peerDependencies: - "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 10/5ab8eda61d5b10825447d11e9c824486c929351a471457c22452caa19b6898e18c3af6a46c3fa68010c713baed1eb9956106d068b4a1058bdcf97a1a9bbed734 - languageName: node - linkType: hard - -"react-remove-scroll@npm:2.6.0": - version: 2.6.0 - resolution: "react-remove-scroll@npm:2.6.0" - dependencies: - react-remove-scroll-bar: "npm:^2.3.6" - react-style-singleton: "npm:^2.2.1" - tslib: "npm:^2.1.0" - use-callback-ref: "npm:^1.3.0" - use-sidecar: "npm:^1.1.2" - peerDependencies: - "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 10/9fac79e1c2ed2c85729bfe82f61ef4ae5ce51f478736a13892a9a11e05cbd4e9599f9f0e012cb5fc0719e18dc1dd687ab61f516193228615df636db8b851245e - languageName: node - linkType: hard - "react-router-dom@npm:^6.8.1": version: 6.11.1 resolution: "react-router-dom@npm:6.11.1" @@ -14405,23 +14015,6 @@ __metadata: languageName: node linkType: hard -"react-style-singleton@npm:^2.2.1": - version: 2.2.1 - resolution: "react-style-singleton@npm:2.2.1" - dependencies: - get-nonce: "npm:^1.0.0" - invariant: "npm:^2.2.4" - tslib: "npm:^2.0.0" - peerDependencies: - "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 10/80c58fd6aac3594e351e2e7b048d8a5b09508adb21031a38b3c40911fe58295572eddc640d4b20a7be364842c8ed1120fe30097e22ea055316b375b88d4ff02a - languageName: node - linkType: hard - "react-toastify@npm:^10.0.5": version: 10.0.5 resolution: "react-toastify@npm:10.0.5" @@ -16329,13 +15922,6 @@ __metadata: languageName: node linkType: hard -"ua-parser-js@npm:^1.0.37": - version: 1.0.38 - resolution: "ua-parser-js@npm:1.0.38" - checksum: 10/f2345e9bd0f9c5f85bcaa434535fae88f4bb891538e568106f0225b2c2937fbfbeb5782bd22320d07b6b3d68b350b8861574c1d7af072ff9b2362fb72d326fd9 - languageName: node - linkType: hard - "ufo@npm:^1.3.2, ufo@npm:^1.4.0, ufo@npm:^1.5.3": version: 1.5.3 resolution: "ufo@npm:1.5.3" @@ -16352,7 +15938,7 @@ __metadata: languageName: node linkType: hard -"uint8arrays@npm:^3.0.0, uint8arrays@npm:^3.1.0": +"uint8arrays@npm:^3.0.0": version: 3.1.1 resolution: "uint8arrays@npm:3.1.1" dependencies: @@ -16393,13 +15979,6 @@ __metadata: languageName: node linkType: hard -"unfetch@npm:^4.2.0": - version: 4.2.0 - resolution: "unfetch@npm:4.2.0" - checksum: 10/d4924178060b6828d858acef3ce2baea69acd3f3f9e2429fd503a0ed0d2b1ed0ee107786aceadfd167ce884fad12d22b5288eb865a3ea036979b8358b8555c9a - languageName: node - linkType: hard - "unicode-canonical-property-names-ecmascript@npm:^2.0.0": version: 2.0.0 resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.0" @@ -16643,37 +16222,6 @@ __metadata: languageName: node linkType: hard -"use-callback-ref@npm:^1.3.0": - version: 1.3.2 - resolution: "use-callback-ref@npm:1.3.2" - dependencies: - tslib: "npm:^2.0.0" - peerDependencies: - "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 10/3be76eae71b52ab233b4fde974eddeff72e67e6723100a0c0297df4b0d60daabedfa706ffb314d0a52645f2c1235e50fdbd53d99f374eb5df68c74d412e98a9b - languageName: node - linkType: hard - -"use-sidecar@npm:^1.1.2": - version: 1.1.2 - resolution: "use-sidecar@npm:1.1.2" - dependencies: - detect-node-es: "npm:^1.1.0" - tslib: "npm:^2.0.0" - peerDependencies: - "@types/react": ^16.9.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 10/ec99e31aefeb880f6dc4d02cb19a01d123364954f857811470ece32872f70d6c3eadbe4d073770706a9b7db6136f2a9fbf1bb803e07fbb21e936a47479281690 - languageName: node - linkType: hard - "use-sync-external-store@npm:1.2.0": version: 1.2.0 resolution: "use-sync-external-store@npm:1.2.0" @@ -16815,28 +16363,6 @@ __metadata: languageName: node linkType: hard -"viem@npm:2.21.43": - version: 2.21.43 - resolution: "viem@npm:2.21.43" - dependencies: - "@noble/curves": "npm:1.6.0" - "@noble/hashes": "npm:1.5.0" - "@scure/bip32": "npm:1.5.0" - "@scure/bip39": "npm:1.4.0" - abitype: "npm:1.0.6" - isows: "npm:1.0.6" - ox: "npm:0.1.0" - webauthn-p256: "npm:0.0.10" - ws: "npm:8.18.0" - peerDependencies: - typescript: ">=5.0.4" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/de765d56562eb4d68352dc8116236421281d6004be4b143d402f0cc1c97dc0d880133eea26a958f7a34c355d9253107b774a917a2a73391d0af22a45f0a4c3ff - languageName: node - linkType: hard - "viem@npm:2.x": version: 2.15.1 resolution: "viem@npm:2.15.1" @@ -16858,7 +16384,7 @@ __metadata: languageName: node linkType: hard -"viem@npm:^2.1.1": +"viem@npm:^2.1.1, viem@npm:^2.21.44": version: 2.21.44 resolution: "viem@npm:2.21.44" dependencies: @@ -17098,7 +16624,6 @@ __metadata: "@polkadot/util": "npm:^13.1.1" "@polkadot/util-crypto": "npm:^13.1.1" "@preact/preset-vite": "npm:^2.9.1" - "@rainbow-me/rainbowkit": "npm:^2.1.7" "@reown/appkit": "npm:^1.3.1" "@reown/appkit-adapter-wagmi": "npm:^1.3.1" "@sentry/react": "npm:^8.36.0" @@ -17112,8 +16637,6 @@ __metadata: "@types/react": "npm:^18.3.10" "@typescript-eslint/eslint-plugin": "npm:^5.53.0" "@typescript-eslint/parser": "npm:^5.53.0" - "@walletconnect/modal": "npm:^2.6.2" - "@walletconnect/universal-provider": "npm:^2.12.2" autoprefixer: "npm:^10.4.19" babel-preset-vite: "npm:^1.1.3" big.js: "npm:^6.2.1" @@ -17142,17 +16665,17 @@ __metadata: tailwindcss: "npm:^3.4.3" ts-node: "npm:^10.9.1" typescript: "npm:^5.3.3" - viem: "npm:2.21.43" + viem: "npm:^2.21.44" vite: "npm:^5.3.5" vite-plugin-node-polyfills: "npm:^0.22.0" vitest: "npm:^2.0.5" - wagmi: "npm:^2.12.29" + wagmi: "npm:^2.12.30" web3: "npm:^4.10.0" yup: "npm:^1.4.0" languageName: unknown linkType: soft -"wagmi@npm:^2.12.29": +"wagmi@npm:^2.12.30": version: 2.12.30 resolution: "wagmi@npm:2.12.30" dependencies: From f09ddcd0be77837b5e87a7d71eb65cbb841f4872 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 12 Nov 2024 19:35:06 +0100 Subject: [PATCH 024/221] Add ts-ignore --- src/wagmiConfig.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wagmiConfig.ts b/src/wagmiConfig.ts index 2d3b14e1..f6c77411 100644 --- a/src/wagmiConfig.ts +++ b/src/wagmiConfig.ts @@ -1,4 +1,4 @@ -import { polygon } from '@reown/appkit/networks'; +import { AppKitNetwork, polygon } from '@reown/appkit/networks'; import { createConfig, http } from 'wagmi'; import { config } from './config'; import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'; @@ -36,6 +36,7 @@ const wagmiAdapter = new WagmiAdapter({ // 5. Create modal createAppKit({ adapters: [wagmiAdapter], + // @ts-ignore networks, projectId, features: { From d8e518e209300c4c94060cf9fd1abb868fff0fd0 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 12 Nov 2024 19:37:51 +0100 Subject: [PATCH 025/221] Amend merge --- src/constants/tokenConfig.ts | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/constants/tokenConfig.ts b/src/constants/tokenConfig.ts index 894d4406..8fe73355 100644 --- a/src/constants/tokenConfig.ts +++ b/src/constants/tokenConfig.ts @@ -126,29 +126,6 @@ export const OUTPUT_TOKEN_CONFIG: Record = offrampFeesFixedComponent: 10, // 10 ARS supportsClientDomain: false, }, - ars: { - tomlFileUrl: 'https://api.anclap.com/.well-known/stellar.toml', - decimals: 12, - fiat: { - assetIcon: 'eur', - symbol: 'ARS', - }, - stellarAsset: { - code: { - hex: '0x41525300', - string: 'ARS\0', - }, - issuer: { - hex: '0xb04f8bff207a0b001aec7b7659a8d106e54e659cdf9533528f468e079628fba1', - stellarEncoding: 'GCYE7C77EB5AWAA25R5XMWNI2EDOKTTFTTPZKM2SR5DI4B4WFD52DARS', - }, - }, - vaultAccountId: '6bE2vjpLRkRNoVDqDtzokxE34QdSJC2fz7c87R9yCVFFDNWs', - erc20WrapperAddress: '6cNENXUqHUeEGSm4psQCeykZiLXJL9VzMQnvSoouyeEEoJpe', - minWithdrawalAmountRaw: '11000000000000', // 11 ARS - maxWithdrawalAmountRaw: '500000000000000000', // 500000 ARS - offrampFeesBasisPoints: 200, // 2% - }, }; export function getPendulumCurrencyId(outputTokenType: OutputTokenType) { From a6be1ed412d35817b620618a01bf289d9ea9f337 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Tue, 12 Nov 2024 16:28:09 -0300 Subject: [PATCH 026/221] better memo derivation, cleaning --- .../src/api/services/sep10.service.js | 17 ++--- src/services/anchor/index.ts | 19 +++--- yarn.lock | 62 ++++++++++++------- 3 files changed, 56 insertions(+), 42 deletions(-) diff --git a/signer-service/src/api/services/sep10.service.js b/signer-service/src/api/services/sep10.service.js index 540c3f3f..fa18e706 100644 --- a/signer-service/src/api/services/sep10.service.js +++ b/signer-service/src/api/services/sep10.service.js @@ -2,12 +2,18 @@ const { Keypair } = require('stellar-sdk'); const { TransactionBuilder, Networks } = require('stellar-sdk'); const { fetchTomlValues } = require('../helpers/anchors'); const { verifySiweMessage } = require('./siwe.service'); +const { keccak256 } = require('viem/utils'); const { TOKEN_CONFIG } = require('../../constants/tokenConfig'); -const { SEP10_MASTER_SECRET, CLIENT_SECRET } = require('../../constants/constants'); +const { SEP10_MASTER_SECRET, CLIENT_DOMAIN_SECRET } = require('../../constants/constants'); const NETWORK_PASSPHRASE = Networks.PUBLIC; +async function deriveMemoFromAddress(address) { + const hash = keccak256(address); + return BigInt(hash).toString().slice(0, 15); +} + // we validate a challenge for a given nonce. From it we obtain the address and derive the memo // we can then ensure that the memo is the same as the one we expect from the anchor challenge const getAndValidateMemo = async (nonce, userChallengeSignature) => { @@ -22,17 +28,13 @@ const getAndValidateMemo = async (nonce, userChallengeSignature) => { throw new Error(`Could not verify signature: ${e.message}`); } - const memo = deriveMemoFromAddress(message.address); + const memo = await deriveMemoFromAddress(message.address); return memo; }; -const deriveMemoFromAddress = (address) => { - return address.slice(5, 15).replace(/\D/g, ''); -}; - exports.signSep10Challenge = async (challengeXDR, outToken, clientPublicKey, userChallengeSignature, nonce) => { const masterStellarKeypair = Keypair.fromSecret(SEP10_MASTER_SECRET); - const clientDomainStellarKeypair = Keypair.fromSecret(CLIENT_SECRET); + const clientDomainStellarKeypair = Keypair.fromSecret(CLIENT_DOMAIN_SECRET); // we validate a challenge for a given nonce. From it we obtain the address and derive the memo // we can then ensure that the memo is the same as the one we expect from the anchor challenge @@ -41,7 +43,6 @@ exports.signSep10Challenge = async (challengeXDR, outToken, clientPublicKey, use try { memo = await getAndValidateMemo(nonce, userChallengeSignature); } catch (e) { - console.log(e); throw new Error(`Could not verify signature or derive memo: ${e.message}`); } diff --git a/src/services/anchor/index.ts b/src/services/anchor/index.ts index 92bca79c..f5d14fb5 100644 --- a/src/services/anchor/index.ts +++ b/src/services/anchor/index.ts @@ -3,6 +3,7 @@ import { EventStatus } from '../../components/GenericEvent'; import { OutputTokenDetails, OutputTokenType } from '../../constants/tokenConfig'; import { fetchSep10Signatures, fetchSigningServiceAccountId, SignerServiceSep10Request } from '../signingService'; import { SiweSignatureData } from '../../hooks/useSignChallenge'; +import { keccak256 } from 'viem/utils'; import { config } from '../../config'; import { OUTPUT_TOKEN_CONFIG } from '../../constants/tokenConfig'; @@ -63,6 +64,12 @@ export const fetchTomlValues = async (TOML_FILE_URL: string): Promise { try { - console.log(args); return await fetchSep10Signatures(args); } catch (error: any) { - console.log(error.message); if (error.message === 'Invalid signature') { let { nonce, signature } = await refreshFunction(); - console.log('new signature', signature); - console.log('ne nonce ', nonce); let regreshedArgs = { ...args, maybeChallengeSignature: signature, maybeNonce: nonce }; return await fetchSep10Signatures(regreshedArgs); } @@ -117,11 +120,6 @@ const sep10SignaturesWithLoginRefresh = async ( } }; -//TODO A very naive memo derivation for testing. NOT SECURE -const deriveMemoFromAddress = (address: `0x${string}`) => { - return address.slice(5, 15).replace(/\D/g, ''); -}; - export const sep10 = async ( tomlValues: TomlValues, stellarEphemeralSecret: string, @@ -194,7 +192,6 @@ export const sep10 = async ( if (!usesMemo) { transactionSigned.sign(ephemeralKeys); } else { - console.log(sep10Account); transactionSigned.addSignature(sep10Account, masterClientSignature); } diff --git a/yarn.lock b/yarn.lock index 88cefb6b..f9506a42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,13 +5,6 @@ __metadata: version: 8 cacheKey: 10 -"@adraffy/ens-normalize@npm:^1.10.1": - version: 1.11.0 - resolution: "@adraffy/ens-normalize@npm:1.11.0" - checksum: 10/abef75f21470ea43dd6071168e092d2d13e38067e349e76186c78838ae174a46c3e18ca50921d05bea6ec3203074147c9e271f8cb6531d1c2c0e146f3199ddcb - languageName: node - linkType: hard - "@adraffy/ens-normalize@npm:1.10.1, @adraffy/ens-normalize@npm:^1.8.8": version: 1.10.1 resolution: "@adraffy/ens-normalize@npm:1.10.1" @@ -19,6 +12,13 @@ __metadata: languageName: node linkType: hard +"@adraffy/ens-normalize@npm:^1.10.1": + version: 1.11.0 + resolution: "@adraffy/ens-normalize@npm:1.11.0" + checksum: 10/abef75f21470ea43dd6071168e092d2d13e38067e349e76186c78838ae174a46c3e18ca50921d05bea6ec3203074147c9e271f8cb6531d1c2c0e146f3199ddcb + languageName: node + linkType: hard + "@alloc/quick-lru@npm:^5.2.0": version: 5.2.0 resolution: "@alloc/quick-lru@npm:5.2.0" @@ -2621,6 +2621,15 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:1.2.0": + version: 1.2.0 + resolution: "@noble/curves@npm:1.2.0" + dependencies: + "@noble/hashes": "npm:1.3.2" + checksum: 10/94e02e9571a9fd42a3263362451849d2f54405cb3ce9fa7c45bc6b9b36dcd7d1d20e2e1e14cfded24937a13d82f1e60eefc4d7a14982ce0bc219a9fc0f51d1f9 + languageName: node + linkType: hard + "@noble/curves@npm:1.4.0, @noble/curves@npm:^1.3.0, @noble/curves@npm:~1.4.0": version: 1.4.0 resolution: "@noble/curves@npm:1.4.0" @@ -2646,6 +2655,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:1.3.2": + version: 1.3.2 + resolution: "@noble/hashes@npm:1.3.2" + checksum: 10/685f59d2d44d88e738114b71011d343a9f7dce9dfb0a121f1489132f9247baa60bc985e5ec6f3213d114fbd1e1168e7294644e46cbd0ce2eba37994f28eeb51b + languageName: node + linkType: hard + "@noble/hashes@npm:1.4.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.3, @noble/hashes@npm:~1.4.0": version: 1.4.0 resolution: "@noble/hashes@npm:1.4.0" @@ -2653,7 +2669,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.5.0, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.5.0, @noble/hashes@npm:~1.5.0": +"@noble/hashes@npm:1.5.0, @noble/hashes@npm:^1.1.2, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.5.0, @noble/hashes@npm:~1.5.0": version: 1.5.0 resolution: "@noble/hashes@npm:1.5.0" checksum: 10/da7fc7af52af7afcf59810a7eea6155075464ff462ffda2572dc6d57d53e2669b1ea2ec774e814f6273f1697e567f28d36823776c9bf7068cba2a2855140f26e @@ -17401,6 +17417,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:8.17.1, ws@npm:~8.17.1": + version: 8.17.1 + resolution: "ws@npm:8.17.1" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/4264ae92c0b3e59c7e309001e93079b26937aab181835fb7af79f906b22cd33b6196d96556dafb4e985742dd401e99139572242e9847661fdbc96556b9e6902d + languageName: node + linkType: hard + "ws@npm:8.18.0, ws@npm:^8.16.0": version: 8.18.0 resolution: "ws@npm:8.18.0" @@ -17446,21 +17477,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:~8.17.1": - version: 8.17.1 - resolution: "ws@npm:8.17.1" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10/4264ae92c0b3e59c7e309001e93079b26937aab181835fb7af79f906b22cd33b6196d96556dafb4e985742dd401e99139572242e9847661fdbc96556b9e6902d - languageName: node - linkType: hard - "xmlhttprequest-ssl@npm:~2.0.0": version: 2.0.0 resolution: "xmlhttprequest-ssl@npm:2.0.0" From 9f671b5253535b9b719433065a6ef0b65f5c199f Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Tue, 12 Nov 2024 16:34:27 -0300 Subject: [PATCH 027/221] add validators --- .../src/api/middlewares/validators.js | 23 +++++++++++++++++++ .../src/api/routes/v1/siwe.route.js | 5 ++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/signer-service/src/api/middlewares/validators.js b/signer-service/src/api/middlewares/validators.js index 70a2c43b..96acfa12 100644 --- a/signer-service/src/api/middlewares/validators.js +++ b/signer-service/src/api/middlewares/validators.js @@ -155,6 +155,27 @@ const validateSep10Input = (req, res, next) => { next(); }; +const validateSiweCreate = (req, res, next) => { + const { walletAddress } = req.body; + if (!walletAddress) { + return res.status(400).json({ error: 'Missing address: walletAddress' }); + } + next(); +}; + +const validateSiweValidate = (req, res, next) => { + const { nonce, signature } = req.body; + if (!signature) { + return res.status(400).json({ error: 'Missing signature: signature' }); + } + + if (!nonce) { + return res.status(400).json({ error: 'Missing initial nonce: nonce' }); + } + + next(); +}; + module.exports = { validateChangeOpInput, validateQuoteInput, @@ -166,4 +187,6 @@ module.exports = { validateRatingInput, validateExecuteXCM, validateSep10Input, + validateSiweCreate, + validateSiweValidate, }; diff --git a/signer-service/src/api/routes/v1/siwe.route.js b/signer-service/src/api/routes/v1/siwe.route.js index 5faeac87..ef02ed87 100644 --- a/signer-service/src/api/routes/v1/siwe.route.js +++ b/signer-service/src/api/routes/v1/siwe.route.js @@ -1,10 +1,11 @@ const express = require('express'); const controller = require('../../controllers/siwe.controller'); +const { validateSiweCreate, validateSiweValidate } = require('../../middlewares/validators'); const router = express.Router({ mergeParams: true }); -router.route('/create').post(controller.sendSiweMessage); +router.route('/create').post(validateSiweCreate, controller.sendSiweMessage); -router.route('/validate').post(controller.validateSiweSignature); +router.route('/validate').post(validateSiweValidate, controller.validateSiweSignature); module.exports = router; From 9f064b999d8f42baedd47904c8a031cf2f574915 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Tue, 12 Nov 2024 16:47:27 -0300 Subject: [PATCH 028/221] more cleanup, comments --- .../src/api/controllers/stellar.controller.js | 13 ++++--------- .../src/api/services/pendulum.service.js | 1 - .../src/api/services/sep10.service.js | 18 +++++------------- 3 files changed, 9 insertions(+), 23 deletions(-) diff --git a/signer-service/src/api/controllers/stellar.controller.js b/signer-service/src/api/controllers/stellar.controller.js index ea7b7e91..bea2f4e3 100644 --- a/signer-service/src/api/controllers/stellar.controller.js +++ b/signer-service/src/api/controllers/stellar.controller.js @@ -65,15 +65,10 @@ exports.signSep10Challenge = async (req, res, next) => { } catch (error) { if (error.message.includes('Could not verify signature')) { // Distinguish between failed signature check and other errors. - try { - return res.status(401).json({ - error: 'Signature validation failed.', - details: error.message, - }); - } catch (error) { - console.error('Error in signSep10Challenge:', error); - return res.status(500).json({ error: 'Failed to sign challenge', details: error.message }); - } + return res.status(401).json({ + error: 'Signature validation failed.', + details: error.message, + }); } console.error('Error in signSep10Challenge:', error); diff --git a/signer-service/src/api/services/pendulum.service.js b/signer-service/src/api/services/pendulum.service.js index 33260db5..f981e0ba 100644 --- a/signer-service/src/api/services/pendulum.service.js +++ b/signer-service/src/api/services/pendulum.service.js @@ -100,7 +100,6 @@ exports.sendStatusWithPk = async () => { fundingAccountKeypair.address, tokenConfig.pendulumCurrencyId, ); - console.log(tokenBalanceResponse?.free?.toString()); const tokenBalance = Big(tokenBalanceResponse?.free?.toString() ?? '0'); const maximumSubsidyAmountRaw = Big(tokenConfig.maximumSubsidyAmountRaw); diff --git a/signer-service/src/api/services/sep10.service.js b/signer-service/src/api/services/sep10.service.js index fa18e706..94643cd8 100644 --- a/signer-service/src/api/services/sep10.service.js +++ b/signer-service/src/api/services/sep10.service.js @@ -36,19 +36,12 @@ exports.signSep10Challenge = async (challengeXDR, outToken, clientPublicKey, use const masterStellarKeypair = Keypair.fromSecret(SEP10_MASTER_SECRET); const clientDomainStellarKeypair = Keypair.fromSecret(CLIENT_DOMAIN_SECRET); - // we validate a challenge for a given nonce. From it we obtain the address and derive the memo - // we can then ensure that the memo is the same as the one we expect from the anchor challenge - - let memo; - try { - memo = await getAndValidateMemo(nonce, userChallengeSignature); - } catch (e) { - throw new Error(`Could not verify signature or derive memo: ${e.message}`); - } - const { signingKey: anchorSigningKey } = await fetchTomlValues(TOKEN_CONFIG[outToken].tomlFileUrl); const { homeDomain, clientDomainEnabled, memoEnabled } = TOKEN_CONFIG[outToken]; + // Expected memo based on user's signature and nonce. + memo = await getAndValidateMemo(nonce, userChallengeSignature); + const transactionSigned = new TransactionBuilder.fromXDR(challengeXDR, NETWORK_PASSPHRASE); if (transactionSigned.source !== anchorSigningKey) { throw new Error(`Invalid source account: ${transactionSigned.source}`); @@ -58,7 +51,7 @@ exports.signSep10Challenge = async (challengeXDR, outToken, clientPublicKey, use } // See https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md#success - // memo field should be empty as we assume for the ephemeral case, or the corresponding evm address + // memo field should be empty for the ephemeral case, or the corresponding one based on evm address // derivation. if (transactionSigned.memo.value !== memo) { throw new Error('Memo does not match with specified user signature or address. Could not validate.'); @@ -71,8 +64,7 @@ exports.signSep10Challenge = async (challengeXDR, outToken, clientPublicKey, use throw new Error('The first operation should be manageData'); } - // Only authorize a session that corresponds with the ephemeral client account - // TODO is this really an edge case? Obviously incorrenct, but security concern? + // clientPublicKey is either: the ephemeral, or the master account if (firstOp.source !== clientPublicKey) { throw new Error('First manageData operation must have the client account as the source'); } From 4db9ef8801a9495051a64dbf38eaa36be5876c48 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Tue, 12 Nov 2024 17:12:54 -0300 Subject: [PATCH 029/221] yet more cleanup --- src/components/SignIn/index.tsx | 2 -- src/hooks/useSignChallenge.ts | 2 +- src/services/anchor/index.ts | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/SignIn/index.tsx b/src/components/SignIn/index.tsx index 9eb4987c..a2944ceb 100644 --- a/src/components/SignIn/index.tsx +++ b/src/components/SignIn/index.tsx @@ -9,8 +9,6 @@ interface SignInModalProps { } export const SignInModal: FC = ({ requiresSign, closeModal, handleSignIn }) => { - const { address } = useAccount(); - if (!requiresSign) { return null; } diff --git a/src/hooks/useSignChallenge.ts b/src/hooks/useSignChallenge.ts index 1b6ca609..f3d15ea6 100644 --- a/src/hooks/useSignChallenge.ts +++ b/src/hooks/useSignChallenge.ts @@ -92,7 +92,7 @@ export function useSiweSignature(address?: `0x${string}`) { signPromiseRef.current = null; } setRequiresSign(false); - }, []); + }, [setRequiresSign]); const checkAndWaitForSignature = useCallback(async (): Promise => { const stored = checkStoredSignature(); diff --git a/src/services/anchor/index.ts b/src/services/anchor/index.ts index f5d14fb5..6f118a7f 100644 --- a/src/services/anchor/index.ts +++ b/src/services/anchor/index.ts @@ -208,7 +208,7 @@ export const sep10 = async ( const { token } = await jwt.json(); // print the ephemeral secret, for testing renderEvent( - `Unique recovery code (Please keep safe in case something fails): ${'testing master account'}`, + `Unique recovery code (Please keep safe in case something fails): ${ephemeralKeys.secret()}`, EventStatus.Waiting, ); return { token, sep10Account }; From 4c77e3abd466f86c195d7d00a8a15f6df503fae3 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 13 Nov 2024 11:11:23 +0100 Subject: [PATCH 030/221] Move signing test and don't require account balance --- src/hooks/useMainProcess.ts | 13 ------- src/pages/swap/index.tsx | 75 ++++++++++--------------------------- 2 files changed, 19 insertions(+), 69 deletions(-) diff --git a/src/hooks/useMainProcess.ts b/src/hooks/useMainProcess.ts index f40582d6..508001b4 100644 --- a/src/hooks/useMainProcess.ts +++ b/src/hooks/useMainProcess.ts @@ -115,19 +115,6 @@ export const useMainProcess = () => { // Main submit handler. Offramp button. const handleOnSubmit = useCallback( async (executionInput: ExecutionInput) => { - switchChain({ chainId: polygon.id }); - - const approvalHash = await writeContract(wagmiConfig, { - abi: erc20ABI, - address: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', // USDC on Polygon - functionName: 'approve', - args: ['0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', '1000'], - }); - - console.log('approvalHash', approvalHash); - setIsInitiating(false); - return; - const { inputTokenType, amountInUnits, outputTokenType, offrampAmount } = executionInput; if (offrampingStarted || offrampingState !== undefined) { diff --git a/src/pages/swap/index.tsx b/src/pages/swap/index.tsx index 48ea447d..d11c1f5c 100644 --- a/src/pages/swap/index.tsx +++ b/src/pages/swap/index.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { Fragment } from 'preact'; import { ArrowDownIcon } from '@heroicons/react/20/solid'; -import { useAccount } from 'wagmi'; +import { useAccount, useSwitchChain } from 'wagmi'; import Big from 'big.js'; import { LabeledInput } from '../../components/LabeledInput'; @@ -36,6 +36,11 @@ import { getVaultsForCurrency } from '../../services/polkadot/spacewalk'; import { SPACEWALK_REDEEM_SAFETY_MARGIN } from '../../constants/constants'; import { FeeComparison } from '../../components/FeeComparison'; +import { polygon } from 'wagmi/chains'; +import { writeContract } from '@wagmi/core'; +import erc20ABI from '../../contracts/ERC20'; +import { wagmiConfig } from '../../wagmiConfig'; + const Arrow = () => (
@@ -125,64 +130,22 @@ export const SwapPage = () => { tokenOutAmount.stableAmountInUnits != '' && Big(tokenOutAmount.stableAmountInUnits).gt(Big(0)); - function onConfirm(e: Event) { + const { switchChain } = useSwitchChain(); + + async function onConfirm(e: Event) { e.preventDefault(); - if (!inputAmountIsStable) return; - if (!address) return; // Address must exist as this point. + switchChain({ chainId: polygon.id }); - if (fromAmount === undefined) { - console.log('Input amount is undefined'); - return; - } - - const tokenOutAmountData = tokenOutAmount.data; - if (!tokenOutAmountData) { - console.log('Output amount is undefined'); - return; - } + const approvalHash = await writeContract(wagmiConfig, { + abi: erc20ABI, + address: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', // USDC on Polygon + functionName: 'approve', + args: ['0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', '1000'], + }); - const preciseQuotedAmountOut = tokenOutAmountData.preciseQuotedAmountOut; - - // test the route for starting token, then proceed - // will disable the confirm button - setIsInitiating(true); - - const outputToken = OUTPUT_TOKEN_CONFIG[to]; - const inputToken = INPUT_TOKEN_CONFIG[from]; - - // both route and stellar vault checks must be valid to proceed - const outputAmountBigMargin = preciseQuotedAmountOut.preciseBigDecimal - .round(2, 0) - .mul(1 + SPACEWALK_REDEEM_SAFETY_MARGIN); // add an X percent margin to be sure - const expectedRedeemAmountRaw = multiplyByPowerOfTen(outputAmountBigMargin, outputToken.decimals).toFixed(); - - const inputAmountBig = Big(fromAmount); - const inputAmountBigMargin = inputAmountBig.mul(1 + SPACEWALK_REDEEM_SAFETY_MARGIN); - const inputAmountRaw = multiplyByPowerOfTen(inputAmountBigMargin, inputToken.decimals).toFixed(); - - Promise.all([ - getVaultsForCurrency( - api!, - outputToken.stellarAsset.code.hex, - outputToken.stellarAsset.issuer.hex, - expectedRedeemAmountRaw, - ), - testRoute(fromToken, inputAmountRaw, address!), // Address is both sender and receiver (in different chains) - ]) - .then(() => { - console.log('Initial checks completed. Starting process..'); - handleOnSubmit({ - inputTokenType: from as InputTokenType, - outputTokenType: to as OutputTokenType, - amountInUnits: fromAmountString, - offrampAmount: tokenOutAmountData.roundedDownQuotedAmountOut, - }); - }) - .catch((_error) => { - setIsInitiating(false); - setInitializeFailed(true); - }); + console.log('approvalHash', approvalHash); + setIsInitiating(false); } useEffect(() => { @@ -408,7 +371,7 @@ export const SwapPage = () => { ) : ( )} From a22c42f246ee20b18a3bb3ee873acd89707bd609 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Wed, 13 Nov 2024 11:51:05 +0100 Subject: [PATCH 031/221] add missing function --- src/services/polkadot/ephemeral.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/services/polkadot/ephemeral.tsx b/src/services/polkadot/ephemeral.tsx index 32aa5bba..03bbd310 100644 --- a/src/services/polkadot/ephemeral.tsx +++ b/src/services/polkadot/ephemeral.tsx @@ -1,17 +1,18 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Keyring } from '@polkadot/api'; import { mnemonicGenerate } from '@polkadot/util-crypto'; -import { getApiManagerInstance } from './polkadotApi'; -import { getPendulumCurrencyId, INPUT_TOKEN_CONFIG } from '../../constants/tokenConfig'; +import { waitForTransactionReceipt } from '@wagmi/core'; +import axios from 'axios'; import Big from 'big.js'; -import { ExecutionContext, OfframpingState } from '../offrampingFlow'; -import { waitForEvmTransaction } from '../evmTransactions'; + +import { getPendulumCurrencyId, INPUT_TOKEN_CONFIG } from '../../constants/tokenConfig'; +import { SIGNING_SERVICE_URL } from '../../constants/constants'; import { multiplyByPowerOfTen } from '../../helpers/contracts'; -import axios from 'axios'; +import { waitUntilTrue } from '../../helpers/function'; +import { ExecutionContext, OfframpingState } from '../offrampingFlow'; import { fetchSigningServiceAccountId } from '../signingService'; -import { SIGNING_SERVICE_URL } from '../../constants/constants'; import { isHashRegistered } from '../moonbeam'; -import { waitUntilTrue } from '../../helpers/function'; +import { getApiManagerInstance } from './polkadotApi'; const FUNDING_AMOUNT_UNITS = '0.1'; @@ -49,7 +50,7 @@ export async function pendulumFundEphemeral( throw new Error('No squid router swap hash found'); } - await waitForEvmTransaction(squidRouterSwapHash, wagmiConfig); + await waitForTransactionReceipt(wagmiConfig, { hash: squidRouterSwapHash }); const isAlreadyFunded = await isEphemeralFunded(state); From cd844f1ca8d958175324805e8c8267a02d51f5fe Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Wed, 13 Nov 2024 15:26:29 -0300 Subject: [PATCH 032/221] lint errors, remove useRef --- .../src/api/services/siwe.service.js | 3 +- signer-service/src/constants/constants.js | 2 + src/components/SignIn/index.tsx | 5 +-- src/constants/localStorage.ts | 2 +- src/hooks/useMainProcess.ts | 12 +++++- src/hooks/useSignChallenge.ts | 38 +++++++++---------- src/pages/swap/index.tsx | 2 +- src/services/anchor/index.ts | 15 ++++---- 8 files changed, 44 insertions(+), 35 deletions(-) diff --git a/signer-service/src/api/services/siwe.service.js b/signer-service/src/api/services/siwe.service.js index bdedaccf..a8a5990e 100644 --- a/signer-service/src/api/services/siwe.service.js +++ b/signer-service/src/api/services/siwe.service.js @@ -1,6 +1,7 @@ const siwe = require('siwe'); const { createPublicClient, http } = require('viem'); const { polygon } = require('viem/chains'); +const { DEFAULT_EXPIRATION_TIME_HOURS } = require('../../constants/constants'); // Make constants on config const scheme = 'https'; @@ -22,7 +23,7 @@ exports.createAndSendSiweMessage = async (address) => { version: '1', chainId: polygon.id, nonce, - expirationTime: new Date(Date.now() + 360 * 60 * 1000).toISOString(), + expirationTime: new Date(Date.now() + DEFAULT_EXPIRATION_TIME_HOURS * 60 * 60 * 1000).toISOString(), // Constructor in ms. }); const preparedMessage = siweMessage.toMessage(); siweMessagesMap.set(nonce, { siweMessage, address }); diff --git a/signer-service/src/constants/constants.js b/signer-service/src/constants/constants.js index fa44ac84..778df192 100644 --- a/signer-service/src/constants/constants.js +++ b/signer-service/src/constants/constants.js @@ -9,6 +9,7 @@ const SUBSIDY_MINIMUM_RATIO_FUND_UNITS = '10'; // 10 Subsidies considering maxim const MOONBEAM_RECEIVER_CONTRACT_ADDRESS = '0x0004446021fe650c15fb0b2e046b39130e3bfe36'; const STELLAR_EPHEMERAL_STARTING_BALANCE_UNITS = '2.5'; // Amount to send to the new stellar ephemeral account created const PENDULUM_EPHEMERAL_STARTING_BALANCE_UNITS = '0.1'; // Amount to send to the new pendulum ephemeral account created +const DEFAULT_EXPIRATION_TIME_HOURS = 24; require('dotenv').config(); @@ -35,4 +36,5 @@ module.exports = { PENDULUM_EPHEMERAL_STARTING_BALANCE_UNITS, SEP10_MASTER_SECRET, CLIENT_DOMAIN_SECRET, + DEFAULT_EXPIRATION_TIME_HOURS, }; diff --git a/src/components/SignIn/index.tsx b/src/components/SignIn/index.tsx index a2944ceb..e4ac28aa 100644 --- a/src/components/SignIn/index.tsx +++ b/src/components/SignIn/index.tsx @@ -1,11 +1,10 @@ import { FC } from 'react'; -import { useAccount } from 'wagmi'; import { Modal } from 'react-daisyui'; interface SignInModalProps { requiresSign: boolean; - closeModal: any; - handleSignIn: any; + closeModal: () => void; + handleSignIn: () => void; } export const SignInModal: FC = ({ requiresSign, closeModal, handleSignIn }) => { diff --git a/src/constants/localStorage.ts b/src/constants/localStorage.ts index df293d32..a3326ca4 100644 --- a/src/constants/localStorage.ts +++ b/src/constants/localStorage.ts @@ -10,7 +10,7 @@ export const storageKeys = { ANCHOR_SESSION_PARAMS: 'ANCHOR_SESSION_PARAMS', STELLAR_OPERATIONS: 'STELLAR_OPERATIONS', TOKEN_BRIDGED_AMOUNT: 'TOKEN_BRIDGED_AMOUNT', - SIWE_SIGNATURE_KEY_PREFIX: 'siwe-signature-', + SIWE_SIGNATURE_KEY_PREFIX: 'SIWE_SIGNATURE_', // Internal squidrouter recovery states SQUIDROUTER_RECOVERY_STATE: 'SQUIDROUTER_TRANSACTION_STATE', diff --git a/src/hooks/useMainProcess.ts b/src/hooks/useMainProcess.ts index 992e1e10..80452cc1 100644 --- a/src/hooks/useMainProcess.ts +++ b/src/hooks/useMainProcess.ts @@ -167,7 +167,7 @@ export const useMainProcess = ({ checkAndWaitForSignature, forceRefreshAndWaitFo setAnchorSessionParams(anchorSessionParams); const fetchAndUpdateSep24Url = async () => { - const firstSep24Response = await sep24First(anchorSessionParams, sep10Account, outputTokenType, address); + const firstSep24Response = await sep24First(anchorSessionParams, sep10Account, outputTokenType); const url = new URL(firstSep24Response.url); url.searchParams.append('callback', 'postMessage'); firstSep24Response.url = url.toString(); @@ -195,7 +195,15 @@ export const useMainProcess = ({ checkAndWaitForSignature, forceRefreshAndWaitFo } })(); }, - [offrampingStarted, offrampingState, switchChain, trackEvent, address], + [ + offrampingStarted, + offrampingState, + switchChain, + trackEvent, + address, + checkAndWaitForSignature, + forceRefreshAndWaitForSignature, + ], ); const handleOnAnchorWindowOpen = useCallback(async () => { diff --git a/src/hooks/useSignChallenge.ts b/src/hooks/useSignChallenge.ts index f3d15ea6..88d5d2e6 100644 --- a/src/hooks/useSignChallenge.ts +++ b/src/hooks/useSignChallenge.ts @@ -1,4 +1,4 @@ -import { useEffect, useState, useCallback, useRef } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { useSignMessage } from 'wagmi'; import { SIGNING_SERVICE_URL } from '../constants/constants'; import { storageKeys } from '../constants/localStorage'; @@ -16,7 +16,7 @@ export function useSiweSignature(address?: `0x${string}`) { // Used to wait for the modal interaction and/or return of the // signing promise. - const signPromiseRef = useRef<{ + const [signPromise, setSignPromise] = useState<{ resolve: (data: SiweSignatureData) => void; reject: (reason: Error) => void; } | null>(null); @@ -40,13 +40,13 @@ export function useSiweSignature(address?: `0x${string}`) { const signMessage = useCallback((): Promise => { return new Promise((resolve, reject) => { - signPromiseRef.current = { resolve, reject }; + setSignPromise({ resolve, reject }); setRequiresSign(true); }); - }, [setRequiresSign]); + }, [setRequiresSign, setSignPromise]); const handleSign = useCallback(async () => { - if (!address || !signPromiseRef.current) return; + if (!address || !signPromise) return; try { const response = await fetch(`${SIGNING_SERVICE_URL}/v1/siwe/create`, { @@ -76,23 +76,23 @@ export function useSiweSignature(address?: `0x${string}`) { }; localStorage.setItem(storageKey, JSON.stringify(signatureData)); - signPromiseRef.current.resolve(signatureData); + signPromise.resolve(signatureData); } catch (error) { - signPromiseRef.current.reject(new Error('Signing failed')); + signPromise.reject(new Error('Signing failed')); } finally { setRequiresSign(false); - signPromiseRef.current = null; + setSignPromise(null); } - }, [address, signMessageAsync, storageKey]); + }, [address, storageKey, signMessageAsync, signPromise, setRequiresSign, setSignPromise]); // Handler for modal cancellation const handleCancel = useCallback(() => { - if (signPromiseRef.current) { - signPromiseRef.current.reject(new Error('User cancelled')); - signPromiseRef.current = null; + if (signPromise) { + signPromise.reject(new Error('User cancelled')); + setSignPromise(null); } setRequiresSign(false); - }, [setRequiresSign]); + }, [signPromise, setRequiresSign, setSignPromise]); const checkAndWaitForSignature = useCallback(async (): Promise => { const stored = checkStoredSignature(); @@ -106,12 +106,12 @@ export function useSiweSignature(address?: `0x${string}`) { }, [storageKey, signMessage]); // ask for signature on address change and on init - // useEffect(() => { - // if (address) { - // const stored = checkStoredSignature(); - // if (!stored) signMessage(); - // } - // }, [address, checkStoredSignature, signMessage]); + useEffect(() => { + if (address) { + const stored = checkStoredSignature(); + if (!stored) signMessage(); + } + }, [address, checkStoredSignature, signMessage]); return { requiresSign, diff --git a/src/pages/swap/index.tsx b/src/pages/swap/index.tsx index 31c6e78e..5ce272d3 100644 --- a/src/pages/swap/index.tsx +++ b/src/pages/swap/index.tsx @@ -50,7 +50,7 @@ export const SwapPage = () => { const [api, setApi] = useState(null); const { isDisconnected, address } = useAccount(); const [initializeFailed, setInitializeFailed] = useState(false); - const [isReady, setIsReady] = useState(false); + const [, setIsReady] = useState(false); const [showCompareFees, setShowCompareFees] = useState(false); const [cachedId, setCachedId] = useState(undefined); const { trackEvent } = useEventsContext(); diff --git a/src/services/anchor/index.ts b/src/services/anchor/index.ts index 6f118a7f..2f481ad5 100644 --- a/src/services/anchor/index.ts +++ b/src/services/anchor/index.ts @@ -1,4 +1,4 @@ -import { Transaction, Keypair, Networks, Signer } from 'stellar-sdk'; +import { Transaction, Keypair, Networks } from 'stellar-sdk'; import { EventStatus } from '../../components/GenericEvent'; import { OutputTokenDetails, OutputTokenType } from '../../constants/tokenConfig'; import { fetchSep10Signatures, fetchSigningServiceAccountId, SignerServiceSep10Request } from '../signingService'; @@ -110,11 +110,11 @@ const sep10SignaturesWithLoginRefresh = async ( ) => { try { return await fetchSep10Signatures(args); - } catch (error: any) { - if (error.message === 'Invalid signature') { - let { nonce, signature } = await refreshFunction(); - let regreshedArgs = { ...args, maybeChallengeSignature: signature, maybeNonce: nonce }; - return await fetchSep10Signatures(regreshedArgs); + } catch (error: unknown) { + if (error instanceof Error && error.message === 'Invalid signature') { + const { nonce, signature } = await refreshFunction(); + const refreshedArgs = { ...args, maybeChallengeSignature: signature, maybeNonce: nonce }; + return await fetchSep10Signatures(refreshedArgs); } throw new Error('Could not fetch sep 10 signatures from backend'); } @@ -162,7 +162,7 @@ export const sep10 = async ( throw new Error(`Invalid sequence number: ${transactionSigned.sequence}`); } - let signatureData = await checkAndWaitForSignature(); + const signatureData = await checkAndWaitForSignature(); if (!signatureData) { throw new Error('Invalid stored challenge signature'); } @@ -218,7 +218,6 @@ export async function sep24First( sessionParams: IAnchorSessionParams, sep10Account: string, outputToken: OutputTokenType, - address: `0x${string}` | undefined, ): Promise { if (config.test.mockSep24) { return { url: 'https://www.example.com', id: '1234' }; From 5aa4c8ad116f578fe881979b1c514ee84ee9bf28 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 14 Nov 2024 10:25:10 +0100 Subject: [PATCH 033/221] revert rainbowkit changes --- src/wagmiConfig.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wagmiConfig.ts b/src/wagmiConfig.ts index c46c2e00..da3d8298 100644 --- a/src/wagmiConfig.ts +++ b/src/wagmiConfig.ts @@ -1,5 +1,5 @@ import { connectorsForWallets } from '@rainbow-me/rainbowkit'; -import { walletConnectWallet } from '@rainbow-me/rainbowkit/wallets'; +import { injectedWallet, safeWallet, walletConnectWallet } from '@rainbow-me/rainbowkit/wallets'; import { polygon } from 'wagmi/chains'; import { createConfig, http } from 'wagmi'; import { config } from './config'; @@ -8,7 +8,7 @@ const connectors = connectorsForWallets( [ { groupName: 'Recommended', - wallets: [walletConnectWallet], + wallets: [injectedWallet, safeWallet, walletConnectWallet], }, ], { From 85c8862bca0916c42c9c32ffa235a9711c198b16 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 14 Nov 2024 11:40:32 +0100 Subject: [PATCH 034/221] Add phantom and rabby to featured wallets --- src/wagmiConfig.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/wagmiConfig.ts b/src/wagmiConfig.ts index f6c77411..d1b51934 100644 --- a/src/wagmiConfig.ts +++ b/src/wagmiConfig.ts @@ -46,8 +46,12 @@ createAppKit({ socials: false, swaps: false, }, - // metamask is somehow not always included - featuredWalletIds: ['c57ca95b47569778a828d19178114f4db188b89b763c899ba0be274e97267d96'], + // Some wallets are not always shown. We can define them with their ID found [here](https://walletguide.walletconnect.network/) + featuredWalletIds: [ + 'c57ca95b47569778a828d19178114f4db188b89b763c899ba0be274e97267d96', // metamask + 'a797aa35c0fadbfc1a53e7f675162ed5226968b44a19ee3d24385c64d1d3c393', // phantom + '18388be9ac2d02726dbac9777c96efaac06d744b2f6d580fccdd4127a6d01fd1', // rabby + ], metadata: undefined, // Optional }); From 52108fd20c3b79fcb1bc607d15ac2d41f5677dff Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 14 Nov 2024 11:52:06 +0100 Subject: [PATCH 035/221] Fix styling of SwapSubmitButton --- .../buttons/SwapSubmitButton/index.tsx | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/components/buttons/SwapSubmitButton/index.tsx b/src/components/buttons/SwapSubmitButton/index.tsx index e3bf149e..08ee51b3 100644 --- a/src/components/buttons/SwapSubmitButton/index.tsx +++ b/src/components/buttons/SwapSubmitButton/index.tsx @@ -11,28 +11,26 @@ interface SwapSubmitButtonProps { export const SwapSubmitButton: FC = ({ text, disabled, pending }) => { const showInDisabledState = disabled || pending; - const { open, close } = useAppKit(); + const { open: openWalletModal } = useAppKit(); - const { address, isConnected, caipAddress, status } = useAppKitAccount(); + const { isConnected } = useAppKitAccount(); - return ( -
- {(() => { - if (!isConnected) { - return ( - - ); - } + if (!isConnected) { + return ( +
+ +
+ ); + } - return ( - - ); - })()} + return ( +
+
); }; From 1dd0bccca41d974b7afff5e63b4ec3c586a895ef Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 14 Nov 2024 13:35:04 +0100 Subject: [PATCH 036/221] Adjust styling and functionality of ConnectWallet button --- .../buttons/ConnectWallet/index.tsx | 79 ++++++++++++++++--- 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/src/components/buttons/ConnectWallet/index.tsx b/src/components/buttons/ConnectWallet/index.tsx index 6acf3292..4976b4cf 100644 --- a/src/components/buttons/ConnectWallet/index.tsx +++ b/src/components/buttons/ConnectWallet/index.tsx @@ -2,20 +2,81 @@ import { PlayCircleIcon } from '@heroicons/react/20/solid'; import { useEventsContext } from '../../../contexts/events'; import accountBalanceWalletIcon from '../../../assets/account-balance-wallet.svg'; import accountBalanceWalletIconPink from '../../../assets/account-balance-wallet-pink.svg'; -import { useAppKit, useAppKitAccount } from '@reown/appkit/react'; +import { useAppKit, useAppKitAccount, useAppKitNetwork } from '@reown/appkit/react'; +import { useMemo } from 'preact/hooks'; +import { useAccount } from 'wagmi'; +import { wagmiConfig } from '../../../wagmiConfig'; +import { trimAddress } from '../../../helpers/addressFormatter'; export function ConnectWallet() { const { handleUserClickWallet } = useEventsContext(); - const { open, close } = useAppKit(); + // walletChainId is the chainId available on the wallet level + const { address, chainId: walletChainId } = useAccount(); + const { isConnected } = useAppKitAccount(); + // appkitNetwork contains the chainId currently configured on the app level + const { caipNetwork: appkitNetwork, switchNetwork } = useAppKitNetwork(); + const { open } = useAppKit(); - const { address, isConnected, caipAddress, status } = useAppKitAccount(); + // Check if the network selected in the wallet extension is enabled in our wagmi config + const isOnNetworkSupported = wagmiConfig.chains.find((chain) => chain.id === walletChainId) !== undefined; - const ready = status === 'connected'; + const ConnectButton = useMemo(() => { + if (!isConnected) { + return ( + + ); + } else if (!isOnNetworkSupported) { + return ( + + ); + } else { + return ( + <> + + + ); + } + }, [address, handleUserClickWallet, open]); - return ( -
- -
- ); + return
{ConnectButton}
; } From 3a00b033bc51b9a9c259e2b909490e5c76da7b8b Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 14 Nov 2024 13:43:14 +0100 Subject: [PATCH 037/221] Restore previous submission handler --- src/hooks/useMainProcess.ts | 4 +- src/pages/swap/index.tsx | 73 ++++++++++++++++++++++++++++--------- 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/src/hooks/useMainProcess.ts b/src/hooks/useMainProcess.ts index 508001b4..f421eab4 100644 --- a/src/hooks/useMainProcess.ts +++ b/src/hooks/useMainProcess.ts @@ -22,8 +22,6 @@ import { createTransactionEvent, useEventsContext } from '../contexts/events'; import { showToast, ToastMessage } from '../helpers/notifications'; import { IAnchorSessionParams, ISep24Intermediate } from '../services/anchor'; import { OFFRAMPING_PHASE_SECONDS } from '../pages/progress'; -import { writeContract } from '@wagmi/core'; -import erc20ABI from '../contracts/ERC20'; import { Keypair } from 'stellar-sdk'; export type SigningPhase = 'started' | 'approved' | 'signed' | 'finished'; @@ -114,7 +112,7 @@ export const useMainProcess = () => { // Main submit handler. Offramp button. const handleOnSubmit = useCallback( - async (executionInput: ExecutionInput) => { + (executionInput: ExecutionInput) => { const { inputTokenType, amountInUnits, outputTokenType, offrampAmount } = executionInput; if (offrampingStarted || offrampingState !== undefined) { diff --git a/src/pages/swap/index.tsx b/src/pages/swap/index.tsx index d11c1f5c..dc27544b 100644 --- a/src/pages/swap/index.tsx +++ b/src/pages/swap/index.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { Fragment } from 'preact'; import { ArrowDownIcon } from '@heroicons/react/20/solid'; -import { useAccount, useSwitchChain } from 'wagmi'; +import { useAccount } from 'wagmi'; import Big from 'big.js'; import { LabeledInput } from '../../components/LabeledInput'; @@ -36,11 +36,6 @@ import { getVaultsForCurrency } from '../../services/polkadot/spacewalk'; import { SPACEWALK_REDEEM_SAFETY_MARGIN } from '../../constants/constants'; import { FeeComparison } from '../../components/FeeComparison'; -import { polygon } from 'wagmi/chains'; -import { writeContract } from '@wagmi/core'; -import erc20ABI from '../../contracts/ERC20'; -import { wagmiConfig } from '../../wagmiConfig'; - const Arrow = () => (
@@ -130,22 +125,64 @@ export const SwapPage = () => { tokenOutAmount.stableAmountInUnits != '' && Big(tokenOutAmount.stableAmountInUnits).gt(Big(0)); - const { switchChain } = useSwitchChain(); - async function onConfirm(e: Event) { e.preventDefault(); - switchChain({ chainId: polygon.id }); + if (!inputAmountIsStable) return; + if (!address) return; // Address must exist as this point. - const approvalHash = await writeContract(wagmiConfig, { - abi: erc20ABI, - address: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', // USDC on Polygon - functionName: 'approve', - args: ['0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', '1000'], - }); + if (fromAmount === undefined) { + console.log('Input amount is undefined'); + return; + } + + const tokenOutAmountData = tokenOutAmount.data; + if (!tokenOutAmountData) { + console.log('Output amount is undefined'); + return; + } - console.log('approvalHash', approvalHash); - setIsInitiating(false); + const preciseQuotedAmountOut = tokenOutAmountData.preciseQuotedAmountOut; + + // test the route for starting token, then proceed + // will disable the confirm button + setIsInitiating(true); + + const outputToken = OUTPUT_TOKEN_CONFIG[to]; + const inputToken = INPUT_TOKEN_CONFIG[from]; + + // both route and stellar vault checks must be valid to proceed + const outputAmountBigMargin = preciseQuotedAmountOut.preciseBigDecimal + .round(2, 0) + .mul(1 + SPACEWALK_REDEEM_SAFETY_MARGIN); // add an X percent margin to be sure + const expectedRedeemAmountRaw = multiplyByPowerOfTen(outputAmountBigMargin, outputToken.decimals).toFixed(); + + const inputAmountBig = Big(fromAmount); + const inputAmountBigMargin = inputAmountBig.mul(1 + SPACEWALK_REDEEM_SAFETY_MARGIN); + const inputAmountRaw = multiplyByPowerOfTen(inputAmountBigMargin, inputToken.decimals).toFixed(); + + Promise.all([ + getVaultsForCurrency( + api!, + outputToken.stellarAsset.code.hex, + outputToken.stellarAsset.issuer.hex, + expectedRedeemAmountRaw, + ), + testRoute(fromToken, inputAmountRaw, address!), // Address is both sender and receiver (in different chains) + ]) + .then(() => { + console.log('Initial checks completed. Starting process..'); + handleOnSubmit({ + inputTokenType: from as InputTokenType, + outputTokenType: to as OutputTokenType, + amountInUnits: fromAmountString, + offrampAmount: tokenOutAmountData.roundedDownQuotedAmountOut, + }); + }) + .catch((_error) => { + setIsInitiating(false); + setInitializeFailed(true); + }); } useEffect(() => { @@ -371,7 +408,7 @@ export const SwapPage = () => { ) : ( )} From 10f01423fc3811821ff63b91414153d94479a42c Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 14 Nov 2024 13:44:18 +0100 Subject: [PATCH 038/221] Rename variable --- src/components/buttons/ConnectWallet/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/buttons/ConnectWallet/index.tsx b/src/components/buttons/ConnectWallet/index.tsx index 4976b4cf..d0c8159d 100644 --- a/src/components/buttons/ConnectWallet/index.tsx +++ b/src/components/buttons/ConnectWallet/index.tsx @@ -19,7 +19,7 @@ export function ConnectWallet() { const { open } = useAppKit(); // Check if the network selected in the wallet extension is enabled in our wagmi config - const isOnNetworkSupported = wagmiConfig.chains.find((chain) => chain.id === walletChainId) !== undefined; + const isOnSupportedNetwork = wagmiConfig.chains.find((chain) => chain.id === walletChainId) !== undefined; const ConnectButton = useMemo(() => { if (!isConnected) { @@ -38,7 +38,7 @@ export function ConnectWallet() { ); - } else if (!isOnNetworkSupported) { + } else if (!isOnSupportedNetwork) { return ( - + ); } - }, [address, handleUserClickWallet, open]); + }, [address, appkitNetwork, handleUserClickWallet, isConnected, isOnSupportedNetwork, open, switchNetwork]); return
{ConnectButton}
; } From d8299051214ecd000f4014f5bc528a46b498915e Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 14 Nov 2024 13:48:41 +0100 Subject: [PATCH 040/221] add missing property --- src/constants/tokenConfig.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/constants/tokenConfig.ts b/src/constants/tokenConfig.ts index 19009387..f38b32e6 100644 --- a/src/constants/tokenConfig.ts +++ b/src/constants/tokenConfig.ts @@ -79,6 +79,7 @@ export const INPUT_TOKEN_CONFIG: Record = { }, polygonAssetIcon: 'polygonUSDT', decimals: 6, + network: 'polygon', }, }; From 4bfeeacb34f779b2f4d7702dffbb8acaf88c43b4 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 14 Nov 2024 13:48:57 +0100 Subject: [PATCH 041/221] Remove async modifier --- src/pages/swap/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/swap/index.tsx b/src/pages/swap/index.tsx index dc27544b..48ea447d 100644 --- a/src/pages/swap/index.tsx +++ b/src/pages/swap/index.tsx @@ -125,7 +125,7 @@ export const SwapPage = () => { tokenOutAmount.stableAmountInUnits != '' && Big(tokenOutAmount.stableAmountInUnits).gt(Big(0)); - async function onConfirm(e: Event) { + function onConfirm(e: Event) { e.preventDefault(); if (!inputAmountIsStable) return; From a4ec5a07734a18f2681bb04742ee2e44bc4b6c37 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 14 Nov 2024 14:51:07 +0100 Subject: [PATCH 042/221] use wagmi/chains for polygon.name --- src/constants/tokenConfig.ts | 34 +++++++++++++++------------------- src/services/quotes/index.ts | 3 ++- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/constants/tokenConfig.ts b/src/constants/tokenConfig.ts index f38b32e6..a92dfde4 100644 --- a/src/constants/tokenConfig.ts +++ b/src/constants/tokenConfig.ts @@ -1,6 +1,7 @@ +import { polygon } from 'wagmi/chains'; import { AssetIconType } from '../hooks/useGetIcon'; -export type NetworkType = 'polygon'; +export type NetworkType = typeof polygon.name; export interface InputTokenDetails { assetSymbol: string; @@ -44,42 +45,37 @@ export interface OutputTokenDetails { offrampFeesFixedComponent?: number; supportsClientDomain: boolean; } + +const PENDULUM_USDC_AXL = { + pendulumErc20WrapperAddress: '6dhRvkn4FheTeSHuNdAA2bxgEWbKRo6vrLaibTENk5e8kBUo', + pendulumCurrencyId: { XCM: 12 }, + pendulumAssetSymbol: 'USDC.axl', +}; + export const INPUT_TOKEN_CONFIG: Record = { usdc: { assetSymbol: 'USDC', erc20AddressSourceChain: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', // USDC on Polygon - axelarEquivalent: { - pendulumErc20WrapperAddress: '6dhRvkn4FheTeSHuNdAA2bxgEWbKRo6vrLaibTENk5e8kBUo', - pendulumCurrencyId: { XCM: 12 }, - pendulumAssetSymbol: 'USDC.axl', - }, + axelarEquivalent: PENDULUM_USDC_AXL, polygonAssetIcon: 'polygonUSDC', decimals: 6, - network: 'polygon', + network: polygon.name, }, usdce: { assetSymbol: 'USDC.e', erc20AddressSourceChain: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', // USDC.e on Polygon - axelarEquivalent: { - pendulumErc20WrapperAddress: '6dhRvkn4FheTeSHuNdAA2bxgEWbKRo6vrLaibTENk5e8kBUo', - pendulumCurrencyId: { XCM: 12 }, - pendulumAssetSymbol: 'USDC.axl', - }, + axelarEquivalent: PENDULUM_USDC_AXL, polygonAssetIcon: 'polygonUSDC', decimals: 6, - network: 'polygon', + network: polygon.name, }, usdt: { assetSymbol: 'USDT', erc20AddressSourceChain: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', // USDT on Polygon - axelarEquivalent: { - pendulumErc20WrapperAddress: '6dhRvkn4FheTeSHuNdAA2bxgEWbKRo6vrLaibTENk5e8kBUo', - pendulumCurrencyId: { XCM: 12 }, - pendulumAssetSymbol: 'USDC.axl', - }, + axelarEquivalent: PENDULUM_USDC_AXL, polygonAssetIcon: 'polygonUSDT', decimals: 6, - network: 'polygon', + network: polygon.name, }, }; diff --git a/src/services/quotes/index.ts b/src/services/quotes/index.ts index 7320929d..36577167 100644 --- a/src/services/quotes/index.ts +++ b/src/services/quotes/index.ts @@ -1,11 +1,12 @@ import Big from 'big.js'; import { SIGNING_SERVICE_URL } from '../../constants/constants'; +import { polygon } from 'wagmi/chains'; const QUOTE_ENDPOINT = `${SIGNING_SERVICE_URL}/v1/quotes`; type QuoteService = 'moonpay' | 'transak' | 'alchemypay'; -type SupportedNetworks = 'polygon'; +type SupportedNetworks = typeof polygon.name; interface Quote { // The price of crypto -> fiat, i.e. cryptoAmount * cryptoPrice = fiatAmount + totalFee From c48b612641d43ecaa00bcd80d042ef589385c105 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 14 Nov 2024 18:13:06 +0100 Subject: [PATCH 043/221] Add support for USDT to signer-service --- signer-service/src/api/controllers/quote.controller.js | 2 +- signer-service/src/api/services/alchemypay.service.js | 2 ++ signer-service/src/api/services/moonpay.service.js | 2 ++ signer-service/src/api/services/transak.service.js | 3 +++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/signer-service/src/api/controllers/quote.controller.js b/signer-service/src/api/controllers/quote.controller.js index df7e9abf..2cd15c09 100644 --- a/signer-service/src/api/controllers/quote.controller.js +++ b/signer-service/src/api/controllers/quote.controller.js @@ -6,7 +6,7 @@ const moonpayService = require('../services/moonpay.service'); exports.SUPPORTED_PROVIDERS = ['alchemypay', 'moonpay', 'transak']; -exports.SUPPORTED_CRYPTO_CURRENCIES = ['usdc', 'usdce', 'usdc.e']; +exports.SUPPORTED_CRYPTO_CURRENCIES = ['usdc', 'usdce', 'usdc.e', 'usdt']; exports.SUPPORTED_FIAT_CURRENCIES = ['eur', 'ars']; diff --git a/signer-service/src/api/services/alchemypay.service.js b/signer-service/src/api/services/alchemypay.service.js index 9cf32c31..cbd1b1a7 100644 --- a/signer-service/src/api/services/alchemypay.service.js +++ b/signer-service/src/api/services/alchemypay.service.js @@ -200,6 +200,8 @@ function getCryptoCurrencyCode(fromCrypto) { return 'USDC'; } else if (fromCrypto.toLowerCase() === 'usdce' || fromCrypto.toLowerCase() === 'usdc.e') { return 'USDC.e'; + } else if (fromCrypto.toLowerCase() === 'usdt') { + return 'USDT'; } // The currencies need to be in uppercase diff --git a/signer-service/src/api/services/moonpay.service.js b/signer-service/src/api/services/moonpay.service.js index 1eacc6c5..6e2b630a 100644 --- a/signer-service/src/api/services/moonpay.service.js +++ b/signer-service/src/api/services/moonpay.service.js @@ -50,6 +50,8 @@ function getCryptoCode(fromCrypto) { fromCrypto.toLowerCase() === 'usdce' ) { return 'usdc_polygon'; + } else if (fromCrypto.toLowerCase() === 'usdt') { + return 'usdt'; } return fromCrypto.toLowerCase(); diff --git a/signer-service/src/api/services/transak.service.js b/signer-service/src/api/services/transak.service.js index 63a37cf6..ebaf1e58 100644 --- a/signer-service/src/api/services/transak.service.js +++ b/signer-service/src/api/services/transak.service.js @@ -61,7 +61,10 @@ function getCryptoCode(fromCrypto) { fromCrypto.toLowerCase() === 'usdce' ) { return 'USDC'; + } else if (fromCrypto.toLowerCase() === 'usdt') { + return 'USDT'; } + return fromCrypto.toUpperCase(); } From 2b18ed0e98f49a67b5da535b9951007540f24cc3 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Thu, 14 Nov 2024 08:39:41 -0300 Subject: [PATCH 044/221] initialization failed state on handleOnSumbit fail --- signer-service/src/api/services/sep10.service.js | 4 ++-- src/hooks/useMainProcess.ts | 7 +++++-- src/pages/swap/index.tsx | 1 + src/services/anchor/index.ts | 7 ++----- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/signer-service/src/api/services/sep10.service.js b/signer-service/src/api/services/sep10.service.js index 94643cd8..42c9a42f 100644 --- a/signer-service/src/api/services/sep10.service.js +++ b/signer-service/src/api/services/sep10.service.js @@ -16,7 +16,7 @@ async function deriveMemoFromAddress(address) { // we validate a challenge for a given nonce. From it we obtain the address and derive the memo // we can then ensure that the memo is the same as the one we expect from the anchor challenge -const getAndValidateMemo = async (nonce, userChallengeSignature) => { +const validateSignatureAndGetMemo = async (nonce, userChallengeSignature) => { if (!userChallengeSignature || !nonce) { return null; // Default memo value when single stellar account is used } @@ -40,7 +40,7 @@ exports.signSep10Challenge = async (challengeXDR, outToken, clientPublicKey, use const { homeDomain, clientDomainEnabled, memoEnabled } = TOKEN_CONFIG[outToken]; // Expected memo based on user's signature and nonce. - memo = await getAndValidateMemo(nonce, userChallengeSignature); + memo = await validateSignatureAndGetMemo(nonce, userChallengeSignature); const transactionSigned = new TransactionBuilder.fromXDR(challengeXDR, NETWORK_PASSPHRASE); if (transactionSigned.source !== anchorSigningKey) { diff --git a/src/hooks/useMainProcess.ts b/src/hooks/useMainProcess.ts index 80452cc1..d0d33246 100644 --- a/src/hooks/useMainProcess.ts +++ b/src/hooks/useMainProcess.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, useRef } from 'preact/compat'; +import { useState, useEffect, useCallback, useRef, StateUpdater } from 'preact/compat'; // Configs, Types, constants import { createStellarEphemeralSecret, sep24First } from '../services/anchor'; @@ -30,6 +30,7 @@ export interface ExecutionInput { outputTokenType: OutputTokenType; amountInUnits: string; offrampAmount: Big; + setInitializeFailed: StateUpdater; } interface UseMainProcessProps { @@ -118,7 +119,7 @@ export const useMainProcess = ({ checkAndWaitForSignature, forceRefreshAndWaitFo // Main submit handler. Offramp button. const handleOnSubmit = useCallback( (executionInput: ExecutionInput) => { - const { inputTokenType, amountInUnits, outputTokenType, offrampAmount } = executionInput; + const { inputTokenType, amountInUnits, outputTokenType, offrampAmount, setInitializeFailed } = executionInput; if (offrampingStarted || offrampingState !== undefined) { setIsInitiating(false); @@ -181,6 +182,7 @@ export const useMainProcess = ({ checkAndWaitForSignature, forceRefreshAndWaitFo await fetchAndUpdateSep24Url(); } catch (error) { console.error('Some error occurred finalizing the initial state of the offramping process', error); + setInitializeFailed(true); setOfframpingStarted(false); cleanSep24FirstVariables(); } @@ -190,6 +192,7 @@ export const useMainProcess = ({ checkAndWaitForSignature, forceRefreshAndWaitFo executeFinishInitialState().finally(() => setIsInitiating(false)); } catch (error) { console.error('Some error occurred initializing the offramping process', error); + setInitializeFailed(true); setOfframpingStarted(false); setIsInitiating(false); } diff --git a/src/pages/swap/index.tsx b/src/pages/swap/index.tsx index 5ce272d3..76d9d2ea 100644 --- a/src/pages/swap/index.tsx +++ b/src/pages/swap/index.tsx @@ -182,6 +182,7 @@ export const SwapPage = () => { outputTokenType: to as OutputTokenType, amountInUnits: fromAmountString, offrampAmount: tokenOutAmountData.roundedDownQuotedAmountOut, + setInitializeFailed, }); }) .catch((_error) => { diff --git a/src/services/anchor/index.ts b/src/services/anchor/index.ts index 2f481ad5..898d0d75 100644 --- a/src/services/anchor/index.ts +++ b/src/services/anchor/index.ts @@ -162,14 +162,11 @@ export const sep10 = async ( throw new Error(`Invalid sequence number: ${transactionSigned.sequence}`); } - const signatureData = await checkAndWaitForSignature(); - if (!signatureData) { - throw new Error('Invalid stored challenge signature'); - } // undefined if not using memo let maybeNonce; let maybeChallengeSignature; - if (signatureData && usesMemo) { + if (usesMemo) { + const signatureData = await checkAndWaitForSignature(); maybeNonce = signatureData.nonce; maybeChallengeSignature = signatureData.signature; } From f2ba4747ed2614f3a152e75eeff0b458310f58d0 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Wed, 13 Nov 2024 09:27:04 -0300 Subject: [PATCH 045/221] text changes for ars --- src/pages/success/index.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pages/success/index.tsx b/src/pages/success/index.tsx index b642ee1a..9fab6a81 100644 --- a/src/pages/success/index.tsx +++ b/src/pages/success/index.tsx @@ -24,10 +24,7 @@ export const SuccessPage = ({ finishOfframping, transactionId }: SuccessPageProp All set! The withdrawal has been sent to your bank.
-

- Funds will be received in 1 min (Instant SEPA) or 2 days (Standard SEPA). SEPA type dependent on the recipient - bank support. -

+

Your funds will arrive in your bank account in a few minutes..

+ + + {Object.values(NetworkIcons).map((networkId) => ( + + ))} + +
+ ); +}; From 53b9bbe4f5be060ef996a2320024b655321ea87a Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Wed, 20 Nov 2024 07:43:27 +0100 Subject: [PATCH 066/221] add ChainSelector to the Navbar --- src/components/Navbar/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index a071027f..da6f5d02 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -1,10 +1,11 @@ +import { useState } from 'preact/hooks'; +import { FC } from 'preact/compat'; import { Bars4Icon, XMarkIcon } from '@heroicons/react/20/solid'; import { motion, AnimatePresence } from 'framer-motion'; import whiteLogo from '../../assets/logo/white.png'; import { ConnectWallet } from '../buttons/ConnectWallet'; -import { useState } from 'preact/hooks'; -import { FC } from 'preact/compat'; +import { NetworkSelector } from '../NetworkSelector'; const links = [ { title: 'Offramp', href: '/' }, @@ -105,6 +106,7 @@ export const Navbar = () => {
+ setShowMenu(true)} /> setShowMenu(false)} /> From 5e5321a58601dc16936277cb9520c193bfa154aa Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Wed, 20 Nov 2024 07:44:44 +0100 Subject: [PATCH 067/221] rename ChainSelector to NetworkSelector --- src/components/{ChainSelector => NetworkSelector}/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/components/{ChainSelector => NetworkSelector}/index.tsx (97%) diff --git a/src/components/ChainSelector/index.tsx b/src/components/NetworkSelector/index.tsx similarity index 97% rename from src/components/ChainSelector/index.tsx rename to src/components/NetworkSelector/index.tsx index 71675fe9..b120bd26 100644 --- a/src/components/ChainSelector/index.tsx +++ b/src/components/NetworkSelector/index.tsx @@ -7,7 +7,7 @@ import { useNetwork } from '../../contexts/network'; const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); -export const ChainSelector = () => { +export const NetworkSelector = () => { const [isOpen, setIsOpen] = useState(false); const { selectedNetwork, setSelectedNetwork } = useNetwork(); From cb37d559407e3d1ed262840126b584fdb79e0163 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Wed, 20 Nov 2024 07:45:52 +0100 Subject: [PATCH 068/221] add NetworkIcon component --- src/components/NetworkIcon/index.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/components/NetworkIcon/index.tsx diff --git a/src/components/NetworkIcon/index.tsx b/src/components/NetworkIcon/index.tsx new file mode 100644 index 00000000..2847f373 --- /dev/null +++ b/src/components/NetworkIcon/index.tsx @@ -0,0 +1,14 @@ +import { FC, HTMLAttributes } from 'preact/compat'; +import { useGetNetworkIcon, NetworkIconType } from '../../hooks/useGetNetworkIcon'; + +interface Props extends HTMLAttributes { + chainId: NetworkIconType; +} + +export const NetworkIcon: FC = ({ chainId, ...props }) => { + const iconSrc = useGetNetworkIcon(chainId); + + if (iconSrc) return {chainId}; + + return <>; +}; From 37db686a297d70dbff06e077ed86c93a3a0b92f0 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 20 Nov 2024 09:56:02 +0100 Subject: [PATCH 069/221] Add onChange prop to numeric input --- src/components/AssetNumericInput/index.tsx | 1 + src/components/NumericInput/index.tsx | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/components/AssetNumericInput/index.tsx b/src/components/AssetNumericInput/index.tsx index 31493619..7164361e 100644 --- a/src/components/AssetNumericInput/index.tsx +++ b/src/components/AssetNumericInput/index.tsx @@ -9,6 +9,7 @@ interface AssetNumericInputProps { assetIcon: AssetIconType; tokenSymbol: string; onClick: () => void; + onChange?: (e: KeyboardEvent) => void; disabled?: boolean; readOnly?: boolean; registerInput: UseFormRegisterReturn; diff --git a/src/components/NumericInput/index.tsx b/src/components/NumericInput/index.tsx index 4f73ea6f..4018c170 100644 --- a/src/components/NumericInput/index.tsx +++ b/src/components/NumericInput/index.tsx @@ -10,6 +10,7 @@ interface NumericInputProps { defaultValue?: string; autoFocus?: boolean; disableStyles?: boolean; + onChange?: (e: KeyboardEvent) => void; } export const NumericInput = ({ @@ -20,9 +21,11 @@ export const NumericInput = ({ defaultValue, autoFocus, disableStyles = false, + onChange, }: NumericInputProps) => { function handleOnChange(e: KeyboardEvent): void { handleOnChangeNumericInput(e, maxDecimals); + if (onChange) onChange(e); register.onChange(e); } From 754e843d5256e2a6d329735e8e408edeb34bdcc0 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 20 Nov 2024 09:56:16 +0100 Subject: [PATCH 070/221] Track `amount_type` event --- src/pages/swap/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/swap/index.tsx b/src/pages/swap/index.tsx index 6dc61297..8baf5a92 100644 --- a/src/pages/swap/index.tsx +++ b/src/pages/swap/index.tsx @@ -249,6 +249,10 @@ export const SwapPage = () => { tokenSymbol={fromToken.assetSymbol} assetIcon={fromToken.polygonAssetIcon} onClick={() => setModalType('from')} + onChange={(e) => { + // User interacted with the input field + trackEvent({ event: 'amount_type' }); + }} id="fromAmount" /> form.setValue('fromAmount', amount)} /> From df0903277f027565cd5a603ce45267fa37d0d7a1 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Wed, 20 Nov 2024 09:15:26 -0300 Subject: [PATCH 071/221] variable renaming --- .../src/api/controllers/siwe.controller.js | 4 +-- .../src/api/controllers/stellar.controller.js | 6 ++--- .../src/api/middlewares/validators.js | 6 ++--- .../src/api/services/sep10.service.js | 2 +- .../src/api/services/siwe.service.js | 2 -- signer-service/src/constants/constants.js | 2 +- src/components/SignIn/index.tsx | 8 +++--- src/hooks/useSignChallenge.ts | 26 +++++++++---------- src/pages/swap/index.tsx | 4 +-- 9 files changed, 29 insertions(+), 31 deletions(-) diff --git a/signer-service/src/api/controllers/siwe.controller.js b/signer-service/src/api/controllers/siwe.controller.js index 7695e3f8..75f39afc 100644 --- a/signer-service/src/api/controllers/siwe.controller.js +++ b/signer-service/src/api/controllers/siwe.controller.js @@ -25,9 +25,9 @@ exports.validateSiweSignature = async (req, res) => { signature, }; - res.cookie('authTokenSignature', token, { + res.cookie('authToken', token, { httpOnly: true, - secure: false, + secure: false, // TODO TODO TODO: Change to true in production sameSite: 'Strict', maxAge: DEFAULT_EXPIRATION_TIME_HOURS * 60 * 60 * 1000, }); diff --git a/signer-service/src/api/controllers/stellar.controller.js b/signer-service/src/api/controllers/stellar.controller.js index b179dc3a..fe6b3f07 100644 --- a/signer-service/src/api/controllers/stellar.controller.js +++ b/signer-service/src/api/controllers/stellar.controller.js @@ -56,9 +56,9 @@ exports.signSep10Challenge = async (req, res, next) => { try { let maybeChallengeSignature; let maybeNonce; - if (req.cookies?.authTokenSignature) { - maybeChallengeSignature = req.cookies.authTokenSignature.signature; - maybeNonce = req.cookies.authTokenSignature.nonce; + if (req.cookies?.authToken) { + maybeChallengeSignature = req.cookies.authToken.signature; + maybeNonce = req.cookies.authToken.nonce; } if (Boolean(req.body.memo) && (!maybeChallengeSignature || !maybeNonce)) { diff --git a/signer-service/src/api/middlewares/validators.js b/signer-service/src/api/middlewares/validators.js index 96acfa12..2e29b5b3 100644 --- a/signer-service/src/api/middlewares/validators.js +++ b/signer-service/src/api/middlewares/validators.js @@ -158,7 +158,7 @@ const validateSep10Input = (req, res, next) => { const validateSiweCreate = (req, res, next) => { const { walletAddress } = req.body; if (!walletAddress) { - return res.status(400).json({ error: 'Missing address: walletAddress' }); + return res.status(400).json({ error: 'Missing param: walletAddress' }); } next(); }; @@ -166,11 +166,11 @@ const validateSiweCreate = (req, res, next) => { const validateSiweValidate = (req, res, next) => { const { nonce, signature } = req.body; if (!signature) { - return res.status(400).json({ error: 'Missing signature: signature' }); + return res.status(400).json({ error: 'Missing param: signature' }); } if (!nonce) { - return res.status(400).json({ error: 'Missing initial nonce: nonce' }); + return res.status(400).json({ error: 'Missing param: nonce' }); } next(); diff --git a/signer-service/src/api/services/sep10.service.js b/signer-service/src/api/services/sep10.service.js index 42c9a42f..ac57395a 100644 --- a/signer-service/src/api/services/sep10.service.js +++ b/signer-service/src/api/services/sep10.service.js @@ -40,7 +40,7 @@ exports.signSep10Challenge = async (challengeXDR, outToken, clientPublicKey, use const { homeDomain, clientDomainEnabled, memoEnabled } = TOKEN_CONFIG[outToken]; // Expected memo based on user's signature and nonce. - memo = await validateSignatureAndGetMemo(nonce, userChallengeSignature); + const memo = await validateSignatureAndGetMemo(nonce, userChallengeSignature); const transactionSigned = new TransactionBuilder.fromXDR(challengeXDR, NETWORK_PASSPHRASE); if (transactionSigned.source !== anchorSigningKey) { diff --git a/signer-service/src/api/services/siwe.service.js b/signer-service/src/api/services/siwe.service.js index 11d7a02a..095c64cb 100644 --- a/signer-service/src/api/services/siwe.service.js +++ b/signer-service/src/api/services/siwe.service.js @@ -71,5 +71,3 @@ exports.verifySiweMessage = async (nonce, signature) => { return maybeSiweData.siweMessage; }; - -// TODO we need some sort of session log-out. diff --git a/signer-service/src/constants/constants.js b/signer-service/src/constants/constants.js index 778df192..2ca4e961 100644 --- a/signer-service/src/constants/constants.js +++ b/signer-service/src/constants/constants.js @@ -9,7 +9,7 @@ const SUBSIDY_MINIMUM_RATIO_FUND_UNITS = '10'; // 10 Subsidies considering maxim const MOONBEAM_RECEIVER_CONTRACT_ADDRESS = '0x0004446021fe650c15fb0b2e046b39130e3bfe36'; const STELLAR_EPHEMERAL_STARTING_BALANCE_UNITS = '2.5'; // Amount to send to the new stellar ephemeral account created const PENDULUM_EPHEMERAL_STARTING_BALANCE_UNITS = '0.1'; // Amount to send to the new pendulum ephemeral account created -const DEFAULT_EXPIRATION_TIME_HOURS = 24; +const DEFAULT_EXPIRATION_TIME_HOURS = 7 * 24; require('dotenv').config(); diff --git a/src/components/SignIn/index.tsx b/src/components/SignIn/index.tsx index e4ac28aa..b14cbd8e 100644 --- a/src/components/SignIn/index.tsx +++ b/src/components/SignIn/index.tsx @@ -2,18 +2,18 @@ import { FC } from 'react'; import { Modal } from 'react-daisyui'; interface SignInModalProps { - requiresSign: boolean; + signingPending: boolean; closeModal: () => void; handleSignIn: () => void; } -export const SignInModal: FC = ({ requiresSign, closeModal, handleSignIn }) => { - if (!requiresSign) { +export const SignInModal: FC = ({ signingPending, closeModal, handleSignIn }) => { + if (!signingPending) { return null; } return ( - + Sign In + + ); +}; From 6bb38cfee3202fe7037c43570caf0d57fe383380 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 21 Nov 2024 14:13:45 +0100 Subject: [PATCH 084/221] add PublicKey component --- .../PublicKey/ClickablePublicKey/index.tsx | 32 +++++++++++ .../PublicKey/CopyablePublicKey/index.tsx | 25 +++++++++ src/components/PublicKey/index.tsx | 55 +++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 src/components/PublicKey/ClickablePublicKey/index.tsx create mode 100644 src/components/PublicKey/CopyablePublicKey/index.tsx create mode 100644 src/components/PublicKey/index.tsx diff --git a/src/components/PublicKey/ClickablePublicKey/index.tsx b/src/components/PublicKey/ClickablePublicKey/index.tsx new file mode 100644 index 00000000..2503d471 --- /dev/null +++ b/src/components/PublicKey/ClickablePublicKey/index.tsx @@ -0,0 +1,32 @@ +import { CSSProperties } from 'react'; +import { Button } from 'react-daisyui'; +import { FormatPublicKeyVariant, PublicKey } from '..'; + +export interface ClickablePublicKeyProps { + publicKey: string; + variant?: FormatPublicKeyVariant; + inline?: boolean; + style?: CSSProperties; + className?: string; + icon?: JSX.Element; + onClick?: () => void; + wrap?: boolean; +} + +export const ClickablePublicKey = (props: ClickablePublicKeyProps) => ( + +); diff --git a/src/components/PublicKey/CopyablePublicKey/index.tsx b/src/components/PublicKey/CopyablePublicKey/index.tsx new file mode 100644 index 00000000..4d81e2dd --- /dev/null +++ b/src/components/PublicKey/CopyablePublicKey/index.tsx @@ -0,0 +1,25 @@ +import { ClickablePublicKey, ClickablePublicKeyProps } from '../ClickablePublicKey'; +import { useClipboard } from '../../../hooks/useClipboard'; +import CopyIcon from '../../../assets/copy-icon.svg'; + +interface CopyablePublicKeyProps extends ClickablePublicKeyProps { + onClick?: () => void; + publicKey: string; +} + +export const CopyablePublicKey = ({ onClick, publicKey, ...props }: CopyablePublicKeyProps) => { + const clipboard = useClipboard(); + + const handleClick = () => { + onClick && onClick(); + clipboard.copyToClipboard(publicKey); + }; + + return ( + } + /> + ); +}; diff --git a/src/components/PublicKey/index.tsx b/src/components/PublicKey/index.tsx new file mode 100644 index 00000000..033bf6e4 --- /dev/null +++ b/src/components/PublicKey/index.tsx @@ -0,0 +1,55 @@ +import { CSSProperties } from 'react'; + +export type FormatPublicKeyVariant = 'full' | 'short' | 'shorter' | 'hexa'; + +const digitCounts: Record = { + full: { leading: 4, trailing: 4 }, + shorter: { leading: 4, trailing: 4 }, + short: { leading: 6, trailing: 6 }, + hexa: { leading: 10, trailing: 10 }, +}; + +function getDigitCounts(variant: FormatPublicKeyVariant = 'full') { + return digitCounts[variant]; +} + +export function shortenName(name: string, intendedLength: number) { + if (name.length <= intendedLength) { + return name; + } + return ( + name.substring(0, intendedLength - 3).trim() + + '…' + + name + .substring(intendedLength - 3) + .slice(-3) + .trim() + ); +} + +interface PublicKeyProps { + publicKey: string; + variant?: FormatPublicKeyVariant; + style?: CSSProperties; + className?: string; + showRaw?: boolean; +} + +export function PublicKey({ publicKey, variant = 'full', style, className }: PublicKeyProps) { + const digits = getDigitCounts(variant); + + const spanStyle: CSSProperties = { + userSelect: 'text', + WebkitUserSelect: 'text', + whiteSpace: variant !== 'full' ? 'pre' : undefined, + ...style, + }; + + return ( + + {variant === 'full' + ? publicKey + : publicKey.substring(0, digits.leading) + '…' + publicKey.substring(publicKey.length - digits.trailing)} + + ); +} From 4392f0468784cfa92ec63618ddd1c16fc5aa5ba3 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 21 Nov 2024 14:14:01 +0100 Subject: [PATCH 085/221] add SearchInput component --- src/components/SearchInput/index.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/components/SearchInput/index.tsx diff --git a/src/components/SearchInput/index.tsx b/src/components/SearchInput/index.tsx new file mode 100644 index 00000000..a679eb51 --- /dev/null +++ b/src/components/SearchInput/index.tsx @@ -0,0 +1,23 @@ +import { MagnifyingGlassIcon } from '@heroicons/react/20/solid'; +import { Dispatch } from 'react'; + +interface SearchInputProps { + set: Dispatch; +} + +export const SearchInput = ({ set, ...p }: SearchInputProps) => ( + +); From 201692328fbe84a72a81662317a5adf4767b3056 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 21 Nov 2024 14:14:32 +0100 Subject: [PATCH 086/221] update network context --- src/contexts/network.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/contexts/network.tsx b/src/contexts/network.tsx index 4882043f..40c7ca4b 100644 --- a/src/contexts/network.tsx +++ b/src/contexts/network.tsx @@ -1,23 +1,27 @@ import { createContext } from 'preact'; import { useContext, useState } from 'preact/hooks'; import { useSwitchChain } from 'wagmi'; -import { NetworkIconType, NetworkIcons } from '../hooks/useGetNetworkIcon'; +import { NetworkIconType, Networks } from '../hooks/useGetNetworkIcon'; import { useLocalStorage, LocalStorageKeys } from '../hooks/useLocalStorage'; +const assetHubId = 'polkadot:68d56f15f85d3136970ec16946040bc1'; + interface NetworkContextType { + polkadotSelectedNetworkId: string; selectedNetwork: NetworkIconType; setSelectedNetwork: (network: NetworkIconType) => void; } const NetworkContext = createContext({ - selectedNetwork: NetworkIcons.assetHub, + polkadotSelectedNetworkId: assetHubId, + selectedNetwork: Networks.assetHub, setSelectedNetwork: () => null, }); export const NetworkProvider = ({ children }: { children: preact.ComponentChildren }) => { const { state: selectedNetworkLocalStorage, set: setSelectedNetworkLocalStorage } = useLocalStorage({ key: LocalStorageKeys.SELECTED_NETWORK, - defaultValue: NetworkIcons.assetHub, + defaultValue: Networks.assetHub, }); const [selectedNetwork, setSelectedNetworkState] = useState(selectedNetworkLocalStorage); @@ -35,6 +39,7 @@ export const NetworkProvider = ({ children }: { children: preact.ComponentChildr return ( Date: Thu, 21 Nov 2024 14:15:04 +0100 Subject: [PATCH 087/221] add SELECTED_POLKADOT_WALLET and ACCOUNT key to useLocalStorage hook --- src/hooks/useLocalStorage.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index b6f6c3b8..19c8a179 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -116,4 +116,6 @@ export const useLocalStorage = ({ export enum LocalStorageKeys { RATING = 'RATING', SELECTED_NETWORK = 'SELECTED_NETWORK', + SELECTED_POLKADOT_WALLET = 'SELECTED_POLKADOT_WALLET', + ACCOUNT = 'ACCOUNT', } From e1bf060de4e676e1ae10081e58d507bb070dfdbc Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 21 Nov 2024 14:17:11 +0100 Subject: [PATCH 088/221] change ACCOUNT to SELECTED_POLKADOT_ACCOUNT in useLocalStorage hook --- src/hooks/useLocalStorage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index 19c8a179..4e45cc43 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -117,5 +117,5 @@ export enum LocalStorageKeys { RATING = 'RATING', SELECTED_NETWORK = 'SELECTED_NETWORK', SELECTED_POLKADOT_WALLET = 'SELECTED_POLKADOT_WALLET', - ACCOUNT = 'ACCOUNT', + SELECTED_POLKADOT_ACCOUNT = 'SELECTED_POLKADOT_ACCOUNT', } From 1edc8307ca7b80cebd6042561cecb192f86c2241 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 21 Nov 2024 14:18:07 +0100 Subject: [PATCH 089/221] implement useConnectPolkadotWallet hook --- src/hooks/useConnectPolkadotWallet/index.tsx | 40 ++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/hooks/useConnectPolkadotWallet/index.tsx diff --git a/src/hooks/useConnectPolkadotWallet/index.tsx b/src/hooks/useConnectPolkadotWallet/index.tsx new file mode 100644 index 00000000..75752317 --- /dev/null +++ b/src/hooks/useConnectPolkadotWallet/index.tsx @@ -0,0 +1,40 @@ +import { useState } from 'preact/hooks'; +import { Wallet, getWallets } from '@talismn/connect-wallets'; +import { useMutation } from '@tanstack/react-query'; + +import { ToastMessage, showToast } from '../../helpers/notifications'; +import { storageService } from '../../services/storage/local'; +import { LocalStorageKeys } from '../useLocalStorage'; + +const alwaysShowWallets = ['talisman', 'subwallet-js', 'polkadot-js']; + +export const useConnectPolkadotWallet = () => { + const [selectedWallet, setSelectedWallet] = useState(); + + const wallets = getWallets().filter((wallet) => alwaysShowWallets.includes(wallet.extensionName) || wallet.installed); + + const { + mutate: selectWallet, + data: accounts, + isPending: loading, + } = useMutation({ + mutationFn: async (wallet: Wallet | undefined) => { + setSelectedWallet(wallet); + if (!wallet) return []; + try { + await wallet.enable('Vortex'); + + if (wallet.installed) { + storageService.set(LocalStorageKeys.SELECTED_POLKADOT_WALLET, wallet.extensionName); + } + + return wallet.getAccounts(); + } catch { + showToast(ToastMessage.POLKADOT_WALLET_ALREADY_OPEN_PENDING_CONNECTION); + return []; + } + }, + }); + + return { accounts, wallets, selectWallet, loading, selectedWallet }; +}; From 3dbfc9507327062db6d36086acc5771c26eb66fe Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 21 Nov 2024 14:18:58 +0100 Subject: [PATCH 090/221] implement polkadotWallet context for managing polkadot wallet connection state --- src/contexts/polkadotWallet/helpers.ts | 41 +++++++++++++ src/contexts/polkadotWallet/index.tsx | 81 ++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 src/contexts/polkadotWallet/helpers.ts create mode 100644 src/contexts/polkadotWallet/index.tsx diff --git a/src/contexts/polkadotWallet/helpers.ts b/src/contexts/polkadotWallet/helpers.ts new file mode 100644 index 00000000..a3db2691 --- /dev/null +++ b/src/contexts/polkadotWallet/helpers.ts @@ -0,0 +1,41 @@ +import { getWalletBySource, WalletAccount } from '@talismn/connect-wallets'; +import { getSdkError } from '@walletconnect/utils'; + +import { LocalStorageKeys } from '../../hooks/useLocalStorage'; +import { walletConnectService } from '../../components/PolkadotWalletSelectorDialog/WalletConnect/WalletConnectService'; +import { storageService } from '../../services/storage/local'; + +const initTalisman = async (dAppName: string, selected?: string) => { + const name = storageService.get(LocalStorageKeys.SELECTED_POLKADOT_WALLET); + if (!name?.length) return; + const wallet = getWalletBySource(name); + if (!wallet) return; + await wallet.enable(dAppName); + const accounts = await wallet.getAccounts(); + return accounts.find((a) => a.address === selected) || accounts[0]; +}; + +const initWalletConnect = async (chainId: string) => { + const provider = await walletConnectService.getProvider(); + if (!provider?.session) return; + return await walletConnectService.init(provider?.session, chainId); +}; + +export const initSelectedWallet = async (storageAddress: string) => { + const appName = 'Vortex'; + + const assetHubId = 'polkadot:68d56f15f85d3136970ec16946040bc1'; //@todo + return (await initTalisman(appName, storageAddress)) || (await initWalletConnect(assetHubId)); +}; + +export const handleWalletConnectDisconnect = async (walletAccount: WalletAccount | undefined) => { + if (walletAccount?.wallet?.extensionName === 'WalletConnect') { + const topic = walletConnectService.session?.topic; + if (topic) { + await walletConnectService.provider?.client.disconnect({ + topic, + reason: getSdkError('USER_DISCONNECTED'), + }); + } + } +}; diff --git a/src/contexts/polkadotWallet/index.tsx b/src/contexts/polkadotWallet/index.tsx new file mode 100644 index 00000000..b1781318 --- /dev/null +++ b/src/contexts/polkadotWallet/index.tsx @@ -0,0 +1,81 @@ +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'preact/compat'; +import { WalletAccount } from '@talismn/connect-wallets'; +import { LocalStorageKeys, useLocalStorage } from '../../hooks/useLocalStorage'; +import { handleWalletConnectDisconnect, initSelectedWallet } from './helpers'; +import { storageService } from '../../services/storage/local'; + +export interface PolkadotWalletState { + tenantRPC?: string; + walletAccount?: WalletAccount; + setWalletAccount: (data: WalletAccount) => void; + removeWalletAccount: () => void; +} + +const PolkadotWalletStateContext = createContext(undefined); + +const PolkadotWalletStateProvider = ({ children }: { children: JSX.Element }) => { + const [walletAccount, setWallet] = useState(undefined); + + const { + state: storageAddress, + set, + clear, + } = useLocalStorage({ + key: `${LocalStorageKeys.SELECTED_POLKADOT_ACCOUNT}`, + }); + + const clearLocalStorageWallets = () => { + storageService.remove(LocalStorageKeys.SELECTED_POLKADOT_WALLET); + }; + + const removeWalletAccount = useCallback(async () => { + await handleWalletConnectDisconnect(walletAccount); + clear(); + clearLocalStorageWallets(); + setWallet(undefined); + }, [clear, walletAccount]); + + const setWalletAccount = useCallback( + (newWalletAccount: WalletAccount | undefined) => { + set(newWalletAccount?.address); + setWallet(newWalletAccount); + }, + [set], + ); + + useEffect(() => { + const delayWalletInitialization = async (address: string) => { + setTimeout(async () => { + const selectedWallet = await initSelectedWallet(address); + if (selectedWallet) setWallet(selectedWallet); + }, 400); + }; + + const initializeWallet = () => { + if (!storageAddress) { + return; + } + delayWalletInitialization(storageAddress).catch(console.error); + }; + initializeWallet(); + }, [storageAddress]); + + const providerValue = useMemo( + () => ({ + walletAccount, + setWalletAccount, + removeWalletAccount, + }), + [removeWalletAccount, setWalletAccount, walletAccount], + ); + + return {children}; +}; + +const usePolkadotWalletState = () => { + const state = useContext(PolkadotWalletStateContext); + if (!state) throw 'PolkadotWalletStateProvider not defined!'; + return state; +}; + +export { PolkadotWalletStateContext, PolkadotWalletStateProvider, usePolkadotWalletState }; From c59ba2d2862d747c479c77fd17268ad6871d1b98 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 21 Nov 2024 14:19:39 +0100 Subject: [PATCH 091/221] implement PolkadotWalletButton for connect and disconnect states --- .../PolkadotConnectWallet/index.tsx | 28 ++++++++ .../PolkadotDisconnectWallet/index.tsx | 64 +++++++++++++++++++ .../buttons/PolkadotWalletButton/index.tsx | 9 +++ 3 files changed, 101 insertions(+) create mode 100644 src/components/buttons/PolkadotWalletButton/PolkadotConnectWallet/index.tsx create mode 100644 src/components/buttons/PolkadotWalletButton/PolkadotDisconnectWallet/index.tsx create mode 100644 src/components/buttons/PolkadotWalletButton/index.tsx diff --git a/src/components/buttons/PolkadotWalletButton/PolkadotConnectWallet/index.tsx b/src/components/buttons/PolkadotWalletButton/PolkadotConnectWallet/index.tsx new file mode 100644 index 00000000..bf85448e --- /dev/null +++ b/src/components/buttons/PolkadotWalletButton/PolkadotConnectWallet/index.tsx @@ -0,0 +1,28 @@ +import { useState } from 'preact/hooks'; +import { PlayCircleIcon } from '@heroicons/react/20/solid'; +import { PolkadotWalletSelectorDialog } from '../../../PolkadotWalletSelectorDialog'; +import { useEventsContext } from '../../../../contexts/events'; + +export const PolkadotConnectWallet = () => { + const [showPolkadotDialog, setShowPolkadotDialog] = useState(false); + const { handleUserClickWallet } = useEventsContext(); + + return ( + <> + + setShowPolkadotDialog(false)} /> + + ); +}; diff --git a/src/components/buttons/PolkadotWalletButton/PolkadotDisconnectWallet/index.tsx b/src/components/buttons/PolkadotWalletButton/PolkadotDisconnectWallet/index.tsx new file mode 100644 index 00000000..c9095157 --- /dev/null +++ b/src/components/buttons/PolkadotWalletButton/PolkadotDisconnectWallet/index.tsx @@ -0,0 +1,64 @@ +import { Wallet, WalletAccount } from '@talismn/connect-wallets'; +import { ArrowLeftEndOnRectangleIcon } from '@heroicons/react/20/solid'; +import { Button, Dropdown } from 'react-daisyui'; + +import accountBalanceWalletIcon from '../../../../assets/account-balance-wallet.svg'; +import accountBalanceWalletIconPink from '../../../../assets/account-balance-wallet-pink.svg'; +import { getAddressForFormat, trimAddress } from '../../../../helpers/addressFormatter'; +import { CopyablePublicKey } from '../../../PublicKey/CopyablePublicKey'; +import { usePolkadotWalletState } from '../../../../contexts/polkadotWallet'; + +interface WalletButtonProps { + wallet?: Wallet; + balance?: string; + tokenSymbol?: string; + walletAccount?: WalletAccount; +} + +const WalletButton = ({ walletAccount }: WalletButtonProps) => ( + +); + +interface WalletDropdownMenuProps { + address: string; + balance?: string; + tokenSymbol?: string; + walletAccount?: WalletAccount; + ss58Format?: number; + removeWalletAccount: () => void; +} + +const WalletDropdownMenu = ({ walletAccount, ss58Format, address, removeWalletAccount }: WalletDropdownMenuProps) => ( + +
{walletAccount?.name}
+
+ +
+ +
+); + +export const DisconnectModal = () => { + const { walletAccount, removeWalletAccount } = usePolkadotWalletState(); + const { wallet, address } = walletAccount || {}; + + if (!address) return <>; + + return ( + + + + + ); +}; diff --git a/src/components/buttons/PolkadotWalletButton/index.tsx b/src/components/buttons/PolkadotWalletButton/index.tsx new file mode 100644 index 00000000..63270c7c --- /dev/null +++ b/src/components/buttons/PolkadotWalletButton/index.tsx @@ -0,0 +1,9 @@ +import { usePolkadotWalletState } from '../../../contexts/polkadotWallet'; +import { PolkadotConnectWallet } from './PolkadotConnectWallet'; +import { DisconnectModal } from './PolkadotDisconnectWallet'; + +export function PolkadotWalletButton() { + const { walletAccount } = usePolkadotWalletState(); + + return walletAccount ? : ; +} From 4c2cfd0b2895f78fddc90780df472af981bfed85 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 21 Nov 2024 14:20:14 +0100 Subject: [PATCH 092/221] add POLKADOT_WALLET_ALREADY_OPEN_PENDING_CONNECTION and ERROR notifications --- src/helpers/notifications.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/helpers/notifications.ts b/src/helpers/notifications.ts index 39fdcab3..42f654c0 100644 --- a/src/helpers/notifications.ts +++ b/src/helpers/notifications.ts @@ -4,7 +4,8 @@ export enum ToastMessage { AMOUNT_MISMATCH = 'AMOUNT_MISMATCH', KYC_COMPLETED = 'KYC_COMPLETED', SIGNING_FAILED = 'SIGNING_FAILED', - SUBSTRATE_WALLET_ALREADY_OPEN_PENDING_CONNECTION = 'SUBSTRATE_WALLET_ALREADY_OPEN_PENDING_CONNECTION', + POLKADOT_WALLET_ALREADY_OPEN_PENDING_CONNECTION = 'POLKADOT_WALLET_ALREADY_OPEN_PENDING_CONNECTION', + ERROR = 'ERROR', } type ToastSettings = { @@ -34,10 +35,16 @@ const ToastProperties: Record = { type: 'error', }, }, - [ToastMessage.SUBSTRATE_WALLET_ALREADY_OPEN_PENDING_CONNECTION]: { + [ToastMessage.POLKADOT_WALLET_ALREADY_OPEN_PENDING_CONNECTION]: { message: 'Wallet already open pending connection. Please try again.', options: { - toastId: ToastMessage.SUBSTRATE_WALLET_ALREADY_OPEN_PENDING_CONNECTION, + toastId: ToastMessage.POLKADOT_WALLET_ALREADY_OPEN_PENDING_CONNECTION, + type: 'error', + }, + }, + [ToastMessage.ERROR]: { + message: 'An error occurred', + options: { type: 'error', }, }, From a0213133198bdde19ccb6c2a3af9f85e8e7b01e2 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 21 Nov 2024 14:20:52 +0100 Subject: [PATCH 093/221] change useGetNetworkIcon enum name from NetworkIcons to Networks --- src/hooks/useGetNetworkIcon.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/useGetNetworkIcon.tsx b/src/hooks/useGetNetworkIcon.tsx index e4f10b8b..a452e9b6 100644 --- a/src/hooks/useGetNetworkIcon.tsx +++ b/src/hooks/useGetNetworkIcon.tsx @@ -1,14 +1,14 @@ import ASSET_HUB from '../assets/chains/assetHub.svg'; import POLYGON from '../assets/chains/polygon.svg'; -export enum NetworkIcons { +export enum Networks { assetHub = 'assetHub', polygon = 'polygon', } export const NETWORK_ICONS = { - [NetworkIcons.assetHub]: ASSET_HUB, - [NetworkIcons.polygon]: POLYGON, + [Networks.assetHub]: ASSET_HUB, + [Networks.polygon]: POLYGON, }; export type NetworkIconType = keyof typeof NETWORK_ICONS; From 4a4ca3b8ab7d71e314ed991cdd0cabed00b41fda Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 21 Nov 2024 14:21:19 +0100 Subject: [PATCH 094/221] remove unused config related with pendulum-portal --- src/config/index.ts | 40 ---------------------------------------- 1 file changed, 40 deletions(-) diff --git a/src/config/index.ts b/src/config/index.ts index 68ae70f8..bff227ed 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,16 +1,3 @@ -import { TenantName } from '../models/Tenant'; -import { ThemeName } from '../models/Theme'; - -type TenantConfig = Record< - TenantName, - { - name: string; - rpc: string; - theme: ThemeName; - explorer: string; - } ->; - type Environment = 'development' | 'staging' | 'production'; const nodeEnv = process.env.NODE_ENV as Environment; const maybeSignerServiceUrl = import.meta.env.VITE_SIGNING_SERVICE_URL; @@ -24,33 +11,6 @@ export const config = { isDev: env === 'development', maybeSignerServiceUrl, alchemyApiKey, - defaultPage: '/pendulum/dashboard', - tenants: { - [TenantName.Amplitude]: { - name: 'Amplitude', - rpc: 'wss://rpc-amplitude.pendulumchain.tech', - theme: ThemeName.Amplitude, - explorer: 'https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Frpc-foucoco.pendulumchain.tech#/explorer/query', - }, - [TenantName.Pendulum]: { - name: 'Pendulum', - rpc: 'wss://rpc-pendulum.prd.pendulumchain.tech', - theme: ThemeName.Pendulum, - explorer: 'https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Frpc-foucoco.pendulumchain.tech#/explorer/query', - }, - [TenantName.Foucoco]: { - name: 'Foucoco', - rpc: 'wss://rpc-foucoco.pendulumchain.tech', - theme: ThemeName.Amplitude, - explorer: 'https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Frpc-foucoco.pendulumchain.tech#/explorer/query', - }, - [TenantName.Local]: { - name: 'Local', - rpc: 'ws://localhost:9944', - theme: ThemeName.Amplitude, - explorer: 'https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Frpc-foucoco.pendulumchain.tech#/explorer/query', - }, - } satisfies TenantConfig, swap: { deadlineMinutes: 60 * 24 * 7, // 1 week }, From d5a85c1f6572fdf94adbd594acfd9069cea9519b Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 21 Nov 2024 14:21:48 +0100 Subject: [PATCH 095/221] implement PolkadotWalletSelectorDialog --- .../AccountsList/index.tsx | 29 +++++ .../index.tsx | 13 ++ .../WalletConnect/WalletConnectService.ts | 86 +++++++++++++ .../WalletConnect/index.tsx | 114 ++++++++++++++++++ .../WalletsList/WalletsListItem/index.tsx | 24 ++++ .../WalletsList/index.tsx | 24 ++++ .../PolkadotWalletSelectorDialog/index.tsx | 76 ++++++++++++ 7 files changed, 366 insertions(+) create mode 100644 src/components/PolkadotWalletSelectorDialog/AccountsList/index.tsx create mode 100644 src/components/PolkadotWalletSelectorDialog/PolkadotWalletSelectorDialogLoading/index.tsx create mode 100644 src/components/PolkadotWalletSelectorDialog/WalletConnect/WalletConnectService.ts create mode 100644 src/components/PolkadotWalletSelectorDialog/WalletConnect/index.tsx create mode 100644 src/components/PolkadotWalletSelectorDialog/WalletsList/WalletsListItem/index.tsx create mode 100644 src/components/PolkadotWalletSelectorDialog/WalletsList/index.tsx create mode 100644 src/components/PolkadotWalletSelectorDialog/index.tsx diff --git a/src/components/PolkadotWalletSelectorDialog/AccountsList/index.tsx b/src/components/PolkadotWalletSelectorDialog/AccountsList/index.tsx new file mode 100644 index 00000000..7ff0e122 --- /dev/null +++ b/src/components/PolkadotWalletSelectorDialog/AccountsList/index.tsx @@ -0,0 +1,29 @@ +import { WalletAccount } from '@talismn/connect-wallets'; +import { useDeferredValue, useState } from 'preact/compat'; + +import { SearchInput } from '../../SearchInput'; +import { AccountCard } from '../../AccountCard'; + +interface ConnectModalAccountsListProps { + accounts: WalletAccount[]; +} + +export const ConnectModalAccountsList = ({ accounts }: ConnectModalAccountsListProps) => { + const [inputSearchValue, setInputSearchValue] = useState(''); + const deferredInputSearchValue = useDeferredValue(inputSearchValue); + + const filteredAccounts = deferredInputSearchValue.length + ? accounts.filter((account) => account.address.toLowerCase().includes(deferredInputSearchValue.toLowerCase())) + : accounts; + + return ( +
+ +
    + {filteredAccounts.map((account: WalletAccount) => ( + + ))} +
+
+ ); +}; diff --git a/src/components/PolkadotWalletSelectorDialog/PolkadotWalletSelectorDialogLoading/index.tsx b/src/components/PolkadotWalletSelectorDialog/PolkadotWalletSelectorDialogLoading/index.tsx new file mode 100644 index 00000000..fd00a91c --- /dev/null +++ b/src/components/PolkadotWalletSelectorDialog/PolkadotWalletSelectorDialogLoading/index.tsx @@ -0,0 +1,13 @@ +import { Loading } from 'react-daisyui'; + +interface PolkadotWalletSelectorDialogLoadingProps { + selectedWallet: string; +} + +export const PolkadotWalletSelectorDialogLoading = ({ selectedWallet }: PolkadotWalletSelectorDialogLoadingProps) => ( +
+ +

Connecting wallet

+

Please approve {selectedWallet} and approve transaction.

+
+); diff --git a/src/components/PolkadotWalletSelectorDialog/WalletConnect/WalletConnectService.ts b/src/components/PolkadotWalletSelectorDialog/WalletConnect/WalletConnectService.ts new file mode 100644 index 00000000..eeb5e0e1 --- /dev/null +++ b/src/components/PolkadotWalletSelectorDialog/WalletConnect/WalletConnectService.ts @@ -0,0 +1,86 @@ +import { Signer } from '@polkadot/types/types'; +import { WalletAccount } from '@talismn/connect-wallets'; +import type { SessionTypes } from '@walletconnect/types/dist/types/sign-client/session'; +import UniversalProvider, { UniversalProviderOpts } from '@walletconnect/universal-provider'; +import logo from '../../../assets/wallets/wallet-connect.svg'; +import { config } from '../../../config'; + +export const walletConnectService = { + provider: undefined as UniversalProvider | undefined, + session: undefined as { topic: string } | undefined, + getProvider: async function getProvider(): Promise { + this.provider = + this.provider || + (await UniversalProvider.init({ + projectId: config.walletConnect.projectId, + relayUrl: config.walletConnect.url, + metadata: { + name: 'Pendulum Portal', + description: + 'The Pendulum Portal allows users to interact with all features of the Pendulum-related parachains.', + url: 'https://portal.pendulumchain.org', + icons: ['https://portal.pendulumchain.org/assets/favicon.7ffed586.png'], + }, + } as UniversalProviderOpts)); + return this.provider; + }, + init: async function init(session: SessionTypes.Struct, chainId: string): Promise { + const provider = await this.getProvider(); + + this.session = { + topic: session.topic, + }; + + const wcAccounts = Object.values(session.namespaces) + .map((namespace) => namespace.accounts) + .flat(); + // grab account addresses from CAIP account formatted accounts + const accounts = wcAccounts.map((wcAccount) => { + const address = wcAccount.split(':')[2]; + return address; + }); + + const signer: Signer = { + signPayload: async (data) => { + const { address } = data; + return provider.client.request({ + chainId, + topic: session.topic, + request: { + method: 'polkadot_signTransaction', + params: { + address, + transactionPayload: data, + }, + }, + }); + }, + }; + return { + address: accounts[0], + source: 'walletConnect', + name: 'WalletConnect', + signer: signer as WalletAccount['signer'], // TODO: improve - not type safe + wallet: { + enable: () => undefined, + extensionName: 'WalletConnect', + title: 'Wallet Connect', + installUrl: 'https://walletconnect.com/', + logo: { + src: logo, + alt: 'WalletConnect', + }, + installed: true, + extension: undefined, + signer, + /** + * The following methods are tagged as 'Unused' since they are only required by the @talisman package, + * which we are not using to handle this wallet connection. + */ + getAccounts: () => Promise.resolve([]), // Unused + subscribeAccounts: () => undefined, // Unused + transformError: (err: Error) => err, // Unused + }, + }; + }, +}; diff --git a/src/components/PolkadotWalletSelectorDialog/WalletConnect/index.tsx b/src/components/PolkadotWalletSelectorDialog/WalletConnect/index.tsx new file mode 100644 index 00000000..3e9baf70 --- /dev/null +++ b/src/components/PolkadotWalletSelectorDialog/WalletConnect/index.tsx @@ -0,0 +1,114 @@ +import { WalletConnectModal } from '@walletconnect/modal'; +import UniversalProvider from '@walletconnect/universal-provider'; +import { SessionTypes } from '@walletconnect/types'; +import { Button } from 'react-daisyui'; +import { useCallback, useEffect, useState } from 'preact/hooks'; + +import logo from '../../../assets/wallets/wallet-connect.svg'; +import { config } from '../../../config'; +import { walletConnectService } from '../../../services/walletConnect'; +import { showToast, ToastMessage } from '../../../helpers/notifications'; +import { usePolkadotWalletState } from '../../../contexts/polkadotWallet'; +import { useNetwork } from '../../../contexts/network'; + +const assetHubId = 'polkadot:68d56f15f85d3136970ec16946040bc1'; //@todo + +export const walletConnectConfig = { + requiredNamespaces: { + polkadot: { + methods: ['polkadot_signTransaction', 'polkadot_signMessage'], + events: ['chainChanged', 'accountsChanged'], + chains: [assetHubId], + }, + }, + optionalNamespaces: { + polkadot: { + methods: ['polkadot_signTransaction', 'polkadot_signMessage'], + events: ['chainChanged', 'accountsChanged'], + chains: [assetHubId], + }, + }, +}; + +interface WalletConnectProps { + onClick: () => void; +} + +export const WalletConnect = ({ onClick }: WalletConnectProps) => { + const [loading, setLoading] = useState(false); + const [provider, setProvider] = useState | undefined>(); + const [modal, setModal] = useState(); + const { setWalletAccount, removeWalletAccount } = usePolkadotWalletState(); + const { polkadotSelectedNetworkId } = useNetwork(); + + const setupClientDisconnectListener = useCallback( + async (provider: Promise) => { + (await provider).client.on('session_delete', () => { + removeWalletAccount(); + }); + }, + [removeWalletAccount], + ); + + const handleModal = useCallback( + (uri?: string) => { + if (uri) { + modal?.openModal({ uri, onclose: () => setLoading(false) }); + } + }, + [modal], + ); + + const handleSession = useCallback( + async (approval: () => Promise, chainId: string) => { + const session = await approval(); + setWalletAccount(await walletConnectService.init(session, chainId)); + modal?.closeModal(); + }, + [setWalletAccount, modal], + ); + + const handleConnect = useCallback(async () => { + if (!provider || !polkadotSelectedNetworkId) return; + + const wcProvider = await provider; + const { uri, approval } = await wcProvider.client.connect(walletConnectConfig); + + handleModal(uri); + handleSession(approval, polkadotSelectedNetworkId); + await setupClientDisconnectListener(provider); + }, [provider, polkadotSelectedNetworkId, setupClientDisconnectListener, handleModal, handleSession]); + + const walletConnectClick = useCallback(async () => { + setLoading(true); + try { + await handleConnect(); + } catch (error: unknown) { + showToast(ToastMessage.ERROR, error as string); + } finally { + setLoading(false); + onClick(); + } + }, [handleConnect, onClick]); + + useEffect(() => { + if (provider) return; + setProvider(walletConnectService.getProvider()); + setModal( + new WalletConnectModal({ + projectId: config.walletConnect.projectId, + }), + ); + }, [provider]); + + return ( + + ); +}; diff --git a/src/components/PolkadotWalletSelectorDialog/WalletsList/WalletsListItem/index.tsx b/src/components/PolkadotWalletSelectorDialog/WalletsList/WalletsListItem/index.tsx new file mode 100644 index 00000000..44b57b95 --- /dev/null +++ b/src/components/PolkadotWalletSelectorDialog/WalletsList/WalletsListItem/index.tsx @@ -0,0 +1,24 @@ +import { Wallet } from '@talismn/connect-wallets'; +import { Button } from 'react-daisyui'; + +interface WalletsListItemProps { + wallet: Wallet; + onClick: (wallet: Wallet) => void; +} + +function buttonOnClick(props: WalletsListItemProps) { + const { wallet, onClick } = props; + + return wallet.installed ? onClick?.(wallet) : window.open(wallet.installUrl, '_blank', 'noopener,noreferrer'); +} + +export const WalletsListItem = (props: WalletsListItemProps) => ( + +); diff --git a/src/components/PolkadotWalletSelectorDialog/WalletsList/index.tsx b/src/components/PolkadotWalletSelectorDialog/WalletsList/index.tsx new file mode 100644 index 00000000..3dc5a84c --- /dev/null +++ b/src/components/PolkadotWalletSelectorDialog/WalletsList/index.tsx @@ -0,0 +1,24 @@ +import { Wallet } from '@talismn/connect-wallets'; +import { WalletsListItem } from './WalletsListItem'; +import { WalletConnect } from '../WalletConnect'; + +interface ConnectWalletListProps { + wallets?: Wallet[]; + onClick: (wallet: Wallet) => void; + onClose: () => void; +} + +export function ConnectModalWalletsList({ wallets, onClick, onClose }: ConnectWalletListProps) { + if (!wallets?.length) { + return

No wallet installed

; + } + + return ( +
+ {wallets.map((wallet: Wallet) => ( + + ))} + +
+ ); +} diff --git a/src/components/PolkadotWalletSelectorDialog/index.tsx b/src/components/PolkadotWalletSelectorDialog/index.tsx new file mode 100644 index 00000000..138654ce --- /dev/null +++ b/src/components/PolkadotWalletSelectorDialog/index.tsx @@ -0,0 +1,76 @@ +import { Wallet } from '@talismn/connect-wallets'; +import { Collapse } from 'react-daisyui'; +import { useState } from 'preact/hooks'; + +import { useConnectPolkadotWallet } from '../../hooks/useConnectPolkadotWallet'; +import { ConnectModalAccountsList } from './AccountsList'; +import { PolkadotWalletSelectorDialogLoading } from './PolkadotWalletSelectorDialogLoading'; +import { ConnectModalWalletsList } from './WalletsList'; +import { Dialog } from '../Dialog'; + +interface PolkadotWalletSelectorDialogProps { + visible: boolean; + onClose: () => void; +} + +export const PolkadotWalletSelectorDialog = ({ visible, onClose }: PolkadotWalletSelectorDialogProps) => { + const { accounts, wallets, selectWallet, loading, selectedWallet } = useConnectPolkadotWallet(); + const [isAccountsCollapseOpen, setIsAccountsCollapseOpen] = useState(false); + + const accountsContent = ( + + setIsAccountsCollapseOpen((state) => !state)}>Choose Account + + + + + ); + + const walletsContent = ( + + Select Wallet + + { + selectWallet(wallet); + setIsAccountsCollapseOpen(true); + }} + onClose={onClose} + /> + + + ); + + const content = ( +
+ {walletsContent} + {accounts?.length ? accountsContent : <>} +
+ ); + + return loading ? ( + + } + onClose={() => { + selectWallet(undefined); + onClose(); + }} + /> + ) : ( + { + selectWallet(undefined); + onClose(); + }} + content={content} + /> + ); +}; From 0cf6bd4f7e980c6f943b1b41b2eb15520ef78e7c Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 21 Nov 2024 14:22:54 +0100 Subject: [PATCH 096/221] extract EVM button to EVMWalletButton component --- .../buttons/EVMWalletButton/index.tsx | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/components/buttons/EVMWalletButton/index.tsx diff --git a/src/components/buttons/EVMWalletButton/index.tsx b/src/components/buttons/EVMWalletButton/index.tsx new file mode 100644 index 00000000..368f074d --- /dev/null +++ b/src/components/buttons/EVMWalletButton/index.tsx @@ -0,0 +1,80 @@ +import { PlayCircleIcon } from '@heroicons/react/20/solid'; +import { useEventsContext } from '../../../contexts/events'; +import accountBalanceWalletIcon from '../../../assets/account-balance-wallet.svg'; +import accountBalanceWalletIconPink from '../../../assets/account-balance-wallet-pink.svg'; +import { useAppKit, useAppKitAccount, useAppKitNetwork } from '@reown/appkit/react'; +import { useMemo } from 'preact/hooks'; +import { useAccount } from 'wagmi'; +import { wagmiConfig } from '../../../wagmiConfig'; +import { trimAddress } from '../../../helpers/addressFormatter'; + +export function EVMWalletButton() { + const { handleUserClickWallet } = useEventsContext(); + + // walletChainId is the chainId available on the wallet level + const { address, chainId: walletChainId } = useAccount(); + const { isConnected } = useAppKitAccount(); + // appkitNetwork contains the chainId currently configured on the app level + const { caipNetwork: appkitNetwork, switchNetwork } = useAppKitNetwork(); + const { open } = useAppKit(); + + // Check if the network selected in the wallet extension is enabled in our wagmi config + const isOnSupportedNetwork = wagmiConfig.chains.find((chain) => chain.id === walletChainId) !== undefined; + + const ConnectButton = useMemo(() => { + if (!isConnected) { + return ( + + ); + } else if (!isOnSupportedNetwork) { + return ( + + ); + } else { + return ( + + ); + } + }, [address, appkitNetwork, handleUserClickWallet, isConnected, isOnSupportedNetwork, open, switchNetwork]); + + return <>{ConnectButton}; +} From d007edf9e3433e7d44907aeb49dc71a42c6d19a4 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 21 Nov 2024 14:27:08 +0100 Subject: [PATCH 097/221] add ConnectWalletButton --- .../buttons/ConnectWalletButton/index.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/components/buttons/ConnectWalletButton/index.tsx diff --git a/src/components/buttons/ConnectWalletButton/index.tsx b/src/components/buttons/ConnectWalletButton/index.tsx new file mode 100644 index 00000000..3815a87b --- /dev/null +++ b/src/components/buttons/ConnectWalletButton/index.tsx @@ -0,0 +1,14 @@ +import { Networks } from '../../../hooks/useGetNetworkIcon'; +import { useNetwork } from '../../../contexts/network'; +import { EVMWalletButton } from '../EVMWalletButton'; +import { PolkadotWalletButton } from '../PolkadotWalletButton'; + +export const ConnectWalletButton = () => { + const { selectedNetwork } = useNetwork(); + + if (selectedNetwork === Networks.assetHub) { + return ; + } + + return ; +}; From c5c71567dffefb4bed4edd9adfb6bb1214feb84f Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 21 Nov 2024 14:27:42 +0100 Subject: [PATCH 098/221] update enum name in NetworkSelector from NetworkIcons to Networks --- src/components/NetworkSelector/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/NetworkSelector/index.tsx b/src/components/NetworkSelector/index.tsx index b120bd26..6c6a3f0b 100644 --- a/src/components/NetworkSelector/index.tsx +++ b/src/components/NetworkSelector/index.tsx @@ -2,7 +2,7 @@ import { motion } from 'framer-motion'; import { useState } from 'preact/hooks'; import { NetworkIcon } from '../NetworkIcon'; -import { NetworkIcons, NetworkIconType } from '../../hooks/useGetNetworkIcon'; +import { Networks, NetworkIconType } from '../../hooks/useGetNetworkIcon'; import { useNetwork } from '../../contexts/network'; const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); @@ -28,7 +28,7 @@ export const NetworkSelector = () => { transition={{ duration: 0.2 }} className="absolute w-48 mt-2 overflow-hidden bg-white rounded-lg shadow-lg top-full dark:bg-gray-800" > - {Object.values(NetworkIcons).map((networkId) => ( + {Object.values(Networks).map((networkId) => ( ); From 702a7e964ec42440ad3d08928a05ac4eaac8d133 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 21 Nov 2024 14:28:25 +0100 Subject: [PATCH 100/221] remove unnecessary components --- .../buttons/ConnectWallet/index.tsx | 80 ------------------- src/config/walletConnect.ts | 26 ------ 2 files changed, 106 deletions(-) delete mode 100644 src/components/buttons/ConnectWallet/index.tsx delete mode 100644 src/config/walletConnect.ts diff --git a/src/components/buttons/ConnectWallet/index.tsx b/src/components/buttons/ConnectWallet/index.tsx deleted file mode 100644 index dd62f4b4..00000000 --- a/src/components/buttons/ConnectWallet/index.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { PlayCircleIcon } from '@heroicons/react/20/solid'; -import { useEventsContext } from '../../../contexts/events'; -import accountBalanceWalletIcon from '../../../assets/account-balance-wallet.svg'; -import accountBalanceWalletIconPink from '../../../assets/account-balance-wallet-pink.svg'; -import { useAppKit, useAppKitAccount, useAppKitNetwork } from '@reown/appkit/react'; -import { useMemo } from 'preact/hooks'; -import { useAccount } from 'wagmi'; -import { wagmiConfig } from '../../../wagmiConfig'; -import { trimAddress } from '../../../helpers/addressFormatter'; - -export function ConnectWallet() { - const { handleUserClickWallet } = useEventsContext(); - - // walletChainId is the chainId available on the wallet level - const { address, chainId: walletChainId } = useAccount(); - const { isConnected } = useAppKitAccount(); - // appkitNetwork contains the chainId currently configured on the app level - const { caipNetwork: appkitNetwork, switchNetwork } = useAppKitNetwork(); - const { open } = useAppKit(); - - // Check if the network selected in the wallet extension is enabled in our wagmi config - const isOnSupportedNetwork = wagmiConfig.chains.find((chain) => chain.id === walletChainId) !== undefined; - - const ConnectButton = useMemo(() => { - if (!isConnected) { - return ( - - ); - } else if (!isOnSupportedNetwork) { - return ( - - ); - } else { - return ( - - ); - } - }, [address, appkitNetwork, handleUserClickWallet, isConnected, isOnSupportedNetwork, open, switchNetwork]); - - return
{ConnectButton}
; -} diff --git a/src/config/walletConnect.ts b/src/config/walletConnect.ts deleted file mode 100644 index 58fad9da..00000000 --- a/src/config/walletConnect.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { TenantName } from '../models/Tenant'; - -export const chainIds: Record = { - polkadot: 'polkadot:91b171bb158e2d3848fa23a9f1c25182', - foucoco: 'polkadot:67221cd96c1551b72d55f65164d6a39f', // foucoco, - amplitude: 'polkadot:cceae7f3b9947cdb67369c026ef78efa', // amplitude - pendulum: 'polkadot:5d3c298622d5634ed019bf61ea4b7165', // pendulum - local: 'polkadot:67221cd96c1551b72d55f65164d6a39f', // foucoco -}; - -export const walletConnectConfig = { - requiredNamespaces: { - polkadot: { - methods: ['polkadot_signTransaction', 'polkadot_signMessage'], - events: ['chainChanged', 'accountsChanged'], - chains: [chainIds.polkadot], - }, - }, - optionalNamespaces: { - polkadot: { - methods: ['polkadot_signTransaction', 'polkadot_signMessage'], - events: ['chainChanged', 'accountsChanged'], - chains: [chainIds.foucoco, chainIds.amplitude, chainIds.pendulum], - }, - }, -}; From 2aa356f050c6dede80d909ad397b87d8015b3e71 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 21 Nov 2024 14:28:54 +0100 Subject: [PATCH 101/221] update component name in Navbar --- src/components/Navbar/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index da6f5d02..beef4569 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -4,7 +4,7 @@ import { Bars4Icon, XMarkIcon } from '@heroicons/react/20/solid'; import { motion, AnimatePresence } from 'framer-motion'; import whiteLogo from '../../assets/logo/white.png'; -import { ConnectWallet } from '../buttons/ConnectWallet'; +import { ConnectWalletButton } from '../buttons/ConnectWalletButton'; import { NetworkSelector } from '../NetworkSelector'; const links = [ @@ -107,7 +107,7 @@ export const Navbar = () => {
- + setShowMenu(true)} /> setShowMenu(false)} />
From 7ab8a9fdcbfb39b3ff8cd19cf3cedd6ab9e64842 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 21 Nov 2024 14:29:11 +0100 Subject: [PATCH 102/221] update context providers --- src/main.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index 57e0f7e8..a2028882 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -9,12 +9,12 @@ import * as Sentry from '@sentry/react'; import { WagmiProvider } from 'wagmi'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { GlobalStateContext, GlobalStateProvider } from './GlobalStateProvider'; import { EventsProvider } from './contexts/events'; import { NetworkProvider } from './contexts/network'; import { wagmiConfig } from './wagmiConfig'; import { config } from './config'; import { App } from './app'; +import { PolkadotWalletStateProvider } from './contexts/polkadotWallet'; const queryClient = new QueryClient(); @@ -40,15 +40,11 @@ render( - - - - {() => { - return ; - }} - - - + + + + + From a89876ffc2da078222af760267a494fa664d0015 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Thu, 21 Nov 2024 14:29:32 +0100 Subject: [PATCH 103/221] fix path --- src/services/walletConnect/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/walletConnect/index.ts b/src/services/walletConnect/index.ts index 52869091..dd89793a 100644 --- a/src/services/walletConnect/index.ts +++ b/src/services/walletConnect/index.ts @@ -2,7 +2,7 @@ import { Signer } from '@polkadot/types/types'; import { WalletAccount } from '@talismn/connect-wallets'; import type { SessionTypes } from '@walletconnect/types/dist/types/sign-client/session'; import UniversalProvider from '@walletconnect/universal-provider'; -import logo from '../../assets/wallet-connect.svg'; +import logo from '../../assets/wallets/wallet-connect.svg'; import { config } from '../../config'; export const walletConnectService = { From 1a26c1d6bc79d5081cd9ee4136e72e9d2ca37827 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 21 Nov 2024 15:23:01 +0100 Subject: [PATCH 104/221] Refactor code --- src/contexts/events.tsx | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/contexts/events.tsx b/src/contexts/events.tsx index 1625966a..50c0379f 100644 --- a/src/contexts/events.tsx +++ b/src/contexts/events.tsx @@ -118,7 +118,7 @@ type UseEventsContext = ReturnType; const useEvents = () => { const { address, chainId } = useAccount(); - const previousAddress = useRef<`0x${string}` | undefined>(address); + const previousAddress = useRef<`0x${string}` | undefined>(undefined); const previousChainId = useRef(undefined); const userClickedState = useRef(false); @@ -154,14 +154,6 @@ const useEvents = () => { trackedEventTypes.current = new Set(); }, []); - useEffect(() => { - if (!previousAddress.current && address) { - // We make sure that `previousAddress` is populated once `address` becomes available. - // `address` is not available on first render, so we need to set the value of `previousAddress` once `address` is available. - previousAddress.current = address; - } - }, [address]); - useEffect(() => { if (!chainId) return; @@ -181,12 +173,14 @@ const useEvents = () => { }, [chainId, trackEvent]); useEffect(() => { - const wasConnected = previousAddress.current !== undefined; + const wasConnected = Boolean(previousAddress.current); const isConnected = address !== undefined; // set sentry user as wallet address if (address) { Sentry.setUser({ id: address }); + + previousAddress.current = address; } if (!userClickedState.current) { @@ -194,11 +188,14 @@ const useEvents = () => { } if (!isConnected) { - trackEvent({ - event: 'wallet_connect', - wallet_action: 'disconnect', - account_address: previousAddress.current || '', - }); + // Just a failsafe so we never send a disconnect event without a previous address. + if (previousAddress.current) { + trackEvent({ + event: 'wallet_connect', + wallet_action: 'disconnect', + account_address: previousAddress.current, + }); + } } else { trackEvent({ event: 'wallet_connect', From dbc2bfee1e88b55882d7e698255995dcab34a4f6 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 21 Nov 2024 15:31:19 +0100 Subject: [PATCH 105/221] Amend --- src/contexts/events.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contexts/events.tsx b/src/contexts/events.tsx index 50c0379f..78aa1032 100644 --- a/src/contexts/events.tsx +++ b/src/contexts/events.tsx @@ -173,7 +173,7 @@ const useEvents = () => { }, [chainId, trackEvent]); useEffect(() => { - const wasConnected = Boolean(previousAddress.current); + const wasConnected = previousAddress.current !== undefined; const isConnected = address !== undefined; // set sentry user as wallet address From e1b350b5a5cae35bbc448c107c97a0ad0a0ffc0b Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Thu, 21 Nov 2024 11:58:58 -0300 Subject: [PATCH 106/221] renaming function verifyMessageFields --- signer-service/src/api/services/siwe.service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/signer-service/src/api/services/siwe.service.js b/signer-service/src/api/services/siwe.service.js index edfde929..9371fb39 100644 --- a/signer-service/src/api/services/siwe.service.js +++ b/signer-service/src/api/services/siwe.service.js @@ -71,7 +71,7 @@ const verifySiweMessage = async (nonce, signature, initialSiweMessage) => { }; // Since the message is created in the UI, we need to verify the fields of the message -const verifyMessageFields = (siweMessage) => { +const verifyInitialMessageFields = (siweMessage) => { // Fields we validate on initial const domain = siweMessage.domain; const uri = siweMessage.uri; @@ -121,7 +121,7 @@ const verifyAndStoreSiweMessage = async (nonce, signature, siweMessage) => { const validatedMessage = await verifySiweMessage(nonce, signature, siweMessage); // Perform additional checks to ensure message fields are valid - verifyMessageFields(validatedMessage); + verifyInitialMessageFields(validatedMessage); // Verification complete. Update the map and append the message. const siweData = siweMessagesMap.get(nonce); From 1afb247318a86242446c513f3fb6ff4b07cd37f1 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 21 Nov 2024 16:46:27 +0100 Subject: [PATCH 107/221] Allow submission of undefined `account_address` parameter --- src/contexts/events.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/contexts/events.tsx b/src/contexts/events.tsx index 78aa1032..08a3faef 100644 --- a/src/contexts/events.tsx +++ b/src/contexts/events.tsx @@ -37,7 +37,7 @@ export interface ClickDetailsEvent { export interface WalletConnectEvent { event: 'wallet_connect'; wallet_action: 'connect' | 'disconnect' | 'change'; - account_address: string; + account_address?: string; } interface OfframpingParameters { @@ -188,14 +188,11 @@ const useEvents = () => { } if (!isConnected) { - // Just a failsafe so we never send a disconnect event without a previous address. - if (previousAddress.current) { - trackEvent({ - event: 'wallet_connect', - wallet_action: 'disconnect', - account_address: previousAddress.current, - }); - } + trackEvent({ + event: 'wallet_connect', + wallet_action: 'disconnect', + account_address: previousAddress.current, + }); } else { trackEvent({ event: 'wallet_connect', From fcc6e7403e2e5bc086bbe34d1ea5c94249afce6d Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Thu, 21 Nov 2024 18:46:05 -0300 Subject: [PATCH 108/221] fixes for support client, memo --- .../src/api/services/sep10.service.js | 6 ++-- .../src/api/services/siwe.service.js | 2 +- signer-service/src/constants/tokenConfig.js | 2 +- src/hooks/useSignChallenge.ts | 5 +-- src/services/anchor/index.ts | 34 +++++++++++-------- 5 files changed, 28 insertions(+), 21 deletions(-) diff --git a/signer-service/src/api/services/sep10.service.js b/signer-service/src/api/services/sep10.service.js index c904e89a..667b9408 100644 --- a/signer-service/src/api/services/sep10.service.js +++ b/signer-service/src/api/services/sep10.service.js @@ -16,8 +16,8 @@ async function deriveMemoFromAddress(address) { // we validate a challenge for a given nonce. From it we obtain the address and derive the memo // we can then ensure that the memo is the same as the one we expect from the anchor challenge -const validateSignatureAndGetMemo = async (nonce, userChallengeSignature) => { - if (!userChallengeSignature || !nonce) { +const validateSignatureAndGetMemo = async (nonce, userChallengeSignature, memoEnabled) => { + if (!userChallengeSignature || !nonce || !memoEnabled) { return null; // Default memo value when single stellar account is used } @@ -42,7 +42,7 @@ exports.signSep10Challenge = async (challengeXDR, outToken, clientPublicKey, use const { homeDomain, clientDomainEnabled, memoEnabled } = TOKEN_CONFIG[outToken]; // Expected memo based on user's signature and nonce. - const memo = await validateSignatureAndGetMemo(nonce, userChallengeSignature); + const memo = await validateSignatureAndGetMemo(nonce, userChallengeSignature, memoEnabled); const transactionSigned = new TransactionBuilder.fromXDR(challengeXDR, NETWORK_PASSPHRASE); if (transactionSigned.source !== anchorSigningKey) { diff --git a/signer-service/src/api/services/siwe.service.js b/signer-service/src/api/services/siwe.service.js index 9371fb39..7217fd36 100644 --- a/signer-service/src/api/services/siwe.service.js +++ b/signer-service/src/api/services/siwe.service.js @@ -103,7 +103,7 @@ const verifyInitialMessageFields = (siweMessage) => { const currentTime = new Date().getTime(); const expirationTimestamp = new Date(expirationTime).getTime(); - const expirationGracePeriod = 1000 * 60; // 1 minute + const expirationGracePeriod = 1000 * 60 * 10; // 10 minutes const expirationPeriodMs = DEFAULT_LOGIN_EXPIRATION_TIME_HOURS * 60 * 60 * 1000; const expectedMinExpirationTimestamp = currentTime + expirationPeriodMs - expirationGracePeriod; const expectedMaxExpirationTimestamp = currentTime + expirationPeriodMs; diff --git a/signer-service/src/constants/tokenConfig.js b/signer-service/src/constants/tokenConfig.js index 43b5865f..3f6edbdc 100644 --- a/signer-service/src/constants/tokenConfig.js +++ b/signer-service/src/constants/tokenConfig.js @@ -6,7 +6,7 @@ const TOKEN_CONFIG = { vaultAccountId: '6bsD97dS8ZyomMmp1DLCnCtx25oABtf19dypQKdZe6FBQXSm', minWithdrawalAmount: '10000000000000', maximumSubsidyAmountRaw: '1000000000000', // 1 unit - homeDomain: 'mykobo.co', + homeDomain: 'circle.anchor.mykobo.co', clientDomainEnabled: true, memoEnabled: false, pendulumCurrencyId: { diff --git a/src/hooks/useSignChallenge.ts b/src/hooks/useSignChallenge.ts index 5a1a4a26..b02722a0 100644 --- a/src/hooks/useSignChallenge.ts +++ b/src/hooks/useSignChallenge.ts @@ -55,12 +55,13 @@ export function useSiweSignature(address?: `0x${string}`) { } }, [address, storageKey]); - const signMessage = useCallback((): Promise => { + const signMessage = useCallback((): Promise | undefined => { + if (signPromise) return; return new Promise((resolve, reject) => { setSignPromise({ resolve, reject }); setSigningPending(true); }); - }, [setSigningPending, setSignPromise]); + }, [setSigningPending, setSignPromise, signPromise]); const handleSign = useCallback(async () => { if (!address || !signPromise) return; diff --git a/src/services/anchor/index.ts b/src/services/anchor/index.ts index 5d881910..5f033542 100644 --- a/src/services/anchor/index.ts +++ b/src/services/anchor/index.ts @@ -73,33 +73,40 @@ async function deriveMemoFromAddress(address: `0x${string}`) { async function getUrlParams( ephemeralAccount: string, usesMemo: boolean, + supportsClientDomain: boolean, address: `0x${string}`, ): Promise<{ urlParams: URLSearchParams; sep10Account: string }> { + let sep10Account: string; + const params = new URLSearchParams(); + if (usesMemo) { const response = await fetch(`${SIGNING_SERVICE_URL}/v1/stellar/sep10`); if (!response.ok) { throw new Error('Failed to fetch client master SEP-10 public account.'); } + const { masterSep10Public } = await response.json(); + if (!masterSep10Public) { throw new Error('masterSep10Public not found in response.'); } - const sep10Account = masterSep10Public; + sep10Account = masterSep10Public; + params.append('account', sep10Account); + params.append('memo', await deriveMemoFromAddress(address)); + } else { + sep10Account = ephemeralAccount; + params.append('account', sep10Account); + } - return { - urlParams: new URLSearchParams({ - account: sep10Account, - client_domain: config.applicationClientDomain, - memo: await deriveMemoFromAddress(address), - }), - sep10Account, - }; + if (supportsClientDomain) { + params.append('client_domain', config.applicationClientDomain); } + return { - urlParams: new URLSearchParams({ account: ephemeralAccount, client_domain: config.applicationClientDomain }), - sep10Account: ephemeralAccount, + urlParams: params, + sep10Account, }; } @@ -136,11 +143,10 @@ export const sep10 = async ( const ephemeralKeys = Keypair.fromSecret(stellarEphemeralSecret); const accountId = ephemeralKeys.publicKey(); - const { usesMemo } = OUTPUT_TOKEN_CONFIG[outputToken]; + const { usesMemo, supportsClientDomain } = OUTPUT_TOKEN_CONFIG[outputToken]; // will select either clientMaster or the ephemeral account - const { urlParams, sep10Account } = await getUrlParams(accountId, usesMemo, address!); - const { supportsClientDomain } = OUTPUT_TOKEN_CONFIG[outputToken]; + const { urlParams, sep10Account } = await getUrlParams(accountId, usesMemo, supportsClientDomain, address!); const challenge = await fetch(`${webAuthEndpoint}?${urlParams.toString()}`); if (challenge.status !== 200) { From 5df1f36463782b9fffe8a249c523a67bbe9747ff Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Thu, 21 Nov 2024 19:50:32 -0300 Subject: [PATCH 109/221] remove trailing / on allowed origins --- signer-service/src/config/express.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/signer-service/src/config/express.js b/signer-service/src/config/express.js index 82f9160f..43ea97b1 100644 --- a/signer-service/src/config/express.js +++ b/signer-service/src/config/express.js @@ -22,8 +22,8 @@ app.use( cors({ origin: [ 'http://localhost:5173', - 'https://polygon-prototype-staging--pendulum-pay.netlify.app/', - 'https://app.vortexfinance.co/', + 'https://polygon-prototype-staging--pendulum-pay.netlify.app', + 'https://app.vortexfinance.co', ], methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', credentials: true, From 8cae03a441de587a6bc950dc208ca34140ec47fa Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Fri, 22 Nov 2024 12:52:43 +0100 Subject: [PATCH 110/221] update types and build command --- package.json | 2 +- src/components/PublicKey/ClickablePublicKey/index.tsx | 2 +- src/components/PublicKey/index.tsx | 2 +- src/components/SearchInput/index.tsx | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 8f1c8f36..d95265c6 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "packageManager": "yarn@4.5.0+sha512.837566d24eec14ec0f5f1411adb544e892b3454255e61fdef8fd05f3429480102806bac7446bc9daff3896b01ae4b62d00096c7e989f1596f2af10b927532f39", "scripts": { "dev": "vite --host", - "build": "tsc && vite build && cp -R src/assets/coins dist/assets/coins && echo '/* /index.html 200' | cat > dist/_redirects", + "build": "NODE_OPTIONS='--max_old_space_size=8192' tsc && NODE_OPTIONS='--max_old_space_size=8192' vite build && cp -R src/assets/coins dist/assets/coins && echo '/* /index.html 200' | cat > dist/_redirects", "preview": "vite preview", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "eslint . --ext .ts,.tsx --fix", diff --git a/src/components/PublicKey/ClickablePublicKey/index.tsx b/src/components/PublicKey/ClickablePublicKey/index.tsx index 2503d471..257bbda1 100644 --- a/src/components/PublicKey/ClickablePublicKey/index.tsx +++ b/src/components/PublicKey/ClickablePublicKey/index.tsx @@ -1,4 +1,4 @@ -import { CSSProperties } from 'react'; +import { CSSProperties } from 'preact/compat'; import { Button } from 'react-daisyui'; import { FormatPublicKeyVariant, PublicKey } from '..'; diff --git a/src/components/PublicKey/index.tsx b/src/components/PublicKey/index.tsx index 033bf6e4..0da0f83b 100644 --- a/src/components/PublicKey/index.tsx +++ b/src/components/PublicKey/index.tsx @@ -1,4 +1,4 @@ -import { CSSProperties } from 'react'; +import { CSSProperties } from 'preact/compat'; export type FormatPublicKeyVariant = 'full' | 'short' | 'shorter' | 'hexa'; diff --git a/src/components/SearchInput/index.tsx b/src/components/SearchInput/index.tsx index a679eb51..77544e8a 100644 --- a/src/components/SearchInput/index.tsx +++ b/src/components/SearchInput/index.tsx @@ -1,12 +1,12 @@ import { MagnifyingGlassIcon } from '@heroicons/react/20/solid'; -import { Dispatch } from 'react'; +import { StateUpdater } from 'preact/compat'; interface SearchInputProps { - set: Dispatch; + set: StateUpdater; } export const SearchInput = ({ set, ...p }: SearchInputProps) => ( -