diff --git a/_raw/locales/en/messages.json b/_raw/locales/en/messages.json index c2a910867dd..e8c1d5cfde8 100644 --- a/_raw/locales/en/messages.json +++ b/_raw/locales/en/messages.json @@ -1371,6 +1371,7 @@ "connectHardwareWallets": "Connect Hardware Wallets", "connectMobileWalletApps": "Connect Mobile Wallet Apps", "connectInstitutionalWallets": "Connect Institutional Wallets", + "connectNarval": "Connect Narval", "createNewSeedPhrase": "Create New Seed Phrase", "importKeystore": "Import KeyStore", "selectImportMethod": "Select Import Method", diff --git a/package.json b/package.json index f939d110b82..acfe6e3bf78 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@metamask/eth-sig-util": "5.1.0", "@metamask/obs-store": "6.0.2", "@metamask/post-message-stream": "8.1.0", + "@narval-xyz/armory-sdk": "0.7.0", "@ngraveio/bc-ur": "1.1.6", "@onekeyfe/hd-core": "0.3.48", "@onekeyfe/hd-web-sdk": "0.3.27", diff --git a/src/background/controller/wallet.ts b/src/background/controller/wallet.ts index eafc9091a29..cad563d13f6 100644 --- a/src/background/controller/wallet.ts +++ b/src/background/controller/wallet.ts @@ -117,6 +117,10 @@ import { import { appIsProd } from '@/utils/env'; import { getRecommendGas, getRecommendNonce } from './walletUtils/sign'; import { waitSignComponentAmounted } from '@/utils/signEvent'; +import NarvalKeyring, { + NarvalAccount, +} from '../service/keyring/eth-narval-keyring'; +import { ArmoryConfig } from '../utils/armory'; const stashKeyrings: Record = {}; @@ -2433,6 +2437,90 @@ export class WalletController extends BaseController { return this._setCurrentAccountFromKeyring(keyring); }; + getNarvalConnections = () => { + const keyrings: NarvalKeyring[] = keyringService.getKeyringsByType( + KEYRING_CLASS.Narval + ); + + return Promise.all( + keyrings.map(async (keyring) => { + const accounts = keyring.getNarvalAccounts(); + const connectionId = keyring.getNarvalConnectionId(); + const credentialPublicKey = keyring.getCredentialAddress(); + + return { + accounts, + connectionId, + credentialPublicKey, + }; + }) + ); + }; + + removeNarvalConnection = async (connectionId: string) => { + const keyring = await keyringService.getNarvalKeyringByConnectionId( + connectionId + ); + await keyringService.removeNarvalConnection(keyring); + return this.getNarvalConnections(); + }; + + connectNarvalAccount = async ( + config: ArmoryConfig + ): Promise<{ accounts: NarvalAccount[]; connectionId: string }> => { + const buffer = Buffer.from( + ethUtil.stripHexPrefix(config.credentialPrivateKey), + 'hex' + ); + + const error = new Error(t('background.error.invalidPrivateKey')); + + try { + if (!ethUtil.isValidPrivate(buffer)) { + throw error; + } + } catch { + throw error; + } + + const armoryConfig = { + ...config, + credentialPrivateKey: ethUtil.addHexPrefix(config.credentialPrivateKey), + } as ArmoryConfig; + + try { + // We first try to fetch the account to be sure the config is correct + const accounts = await NarvalKeyring.fetchNarvalAccounts(armoryConfig); + // If the config is correct, we save it as part of the keyrings + const keyring = await keyringService.connectNarvalAccount(armoryConfig); + const connectionId = keyring.getNarvalConnectionId(); + + return { accounts, connectionId }; + } catch (err) { + if (['FORBIDDEN', 'FAILED'].includes(err?.message)) { + // If this error is thrown from fetchNarvalAccounts it means that the config + // is correct and we can save it as part of the keyrings + await keyringService.connectNarvalAccount(armoryConfig); + } + + throw err; + } + }; + + fetchNarvalAccounts = async (connectionId: string) => { + const keyring = await keyringService.getNarvalKeyringByConnectionId( + connectionId + ); + return keyring.fetchNarvalAccounts(); + }; + + selectNarvalAccounts = ( + connectionId: string, + accounts: NarvalAccount[] + ): Promise => { + return keyringService.selectNarvalAccounts(connectionId, accounts); + }; + // json format is from "https://github.com/SilentCicero/ethereumjs-accounts" // or "https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition" // for example: https://www.myetherwallet.com/create-wallet diff --git a/src/background/service/keyring/eth-narval-keyring.ts b/src/background/service/keyring/eth-narval-keyring.ts new file mode 100644 index 00000000000..af45d6312a6 --- /dev/null +++ b/src/background/service/keyring/eth-narval-keyring.ts @@ -0,0 +1,344 @@ +import { JsonTx, TransactionFactory, TypedTransaction } from '@ethereumjs/tx'; +import { SignTypedDataVersion } from '@metamask/eth-sig-util'; +import { EventEmitter } from 'events'; +import { Hex, parseTransaction, toHex } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { hash } from '@narval-xyz/armory-sdk/signature'; +import { + ArmoryConfig, + getArmoryAccount, + getArmoryClient, + listArmoryAccounts, +} from '../../utils/armory'; +import { + Eip712TypedData, + TransactionRequest, +} from '@narval-xyz/armory-sdk/policy-engine-shared'; + +const TYPE = 'Narval'; + +export type NarvalAccount = { + accountId: string; + address: string; +}; + +type KeyringOpt = { + withAppKeyOrigin?: string; + version?: SignTypedDataVersion | string; +}; + +type SerializedState = { + armoryConfig: ArmoryConfig; + accounts?: NarvalAccount[]; +}; + +// export default class NarvalKeyring implements Keyring { +export default class NarvalKeyring extends EventEmitter { + #armoryConfig: ArmoryConfig | undefined; + + #accounts: NarvalAccount[]; + + readonly type: string = TYPE; + + static type: string = TYPE; + + static getCredentialAddress(credentialPrivateKey: Hex) { + const account = privateKeyToAccount(credentialPrivateKey); + + return account.address; + } + + static getNarvalConnectionId(config: ArmoryConfig) { + const { credentialPrivateKey, ...armoryConfig } = config; + + const credentialAddress = NarvalKeyring.getCredentialAddress( + credentialPrivateKey + ); + + return hash({ + ...armoryConfig, + credentialAddress, + }); + } + + static async fetchNarvalAccounts(config: ArmoryConfig) { + try { + const armoryClient = await getArmoryClient(config); + + const accounts = (await listArmoryAccounts(armoryClient)).map( + ({ id: accountId, address }) => ({ + accountId, + address, + }) + ); + + return accounts; + } catch (err) { + if ( + err.message === 'Unauthorized' && + err?.context?.authorization?.status + ) { + throw new Error(err.context.authorization.status); + } + + throw err; + } + } + + constructor(state: SerializedState | undefined) { + super(); + this.#accounts = state?.accounts || []; + + /* istanbul ignore next: It's not possible to write a unit test for this, because a constructor isn't allowed + * to be async. Jest can't await the constructor, and when the error gets thrown, Jest can't catch it. */ + if (state) { + this.deserialize(state).catch((error: Error) => { + throw new Error(`Problem deserializing NarvalKeyring ${error.message}`); + }); + } + } + + async serialize() { + return { + armoryConfig: this.#armoryConfig, + accounts: this.#accounts, + }; + } + + async deserialize(state: SerializedState) { + this.#armoryConfig = state.armoryConfig; + this.#accounts = state.accounts || []; + } + + getCredentialAddress() { + if (!this.#armoryConfig) { + throw new Error('Narval not configured'); + } + + return NarvalKeyring.getCredentialAddress( + this.#armoryConfig.credentialPrivateKey + ); + } + + getNarvalConnectionId() { + if (!this.#armoryConfig) { + throw new Error('Narval not configured'); + } + + return NarvalKeyring.getNarvalConnectionId(this.#armoryConfig); + } + + async fetchNarvalAccounts(): Promise { + if (!this.#armoryConfig) { + throw new Error('Narval not configured'); + } + + return NarvalKeyring.fetchNarvalAccounts(this.#armoryConfig); + } + + getNarvalAccounts(): NarvalAccount[] { + return this.#accounts; + } + + setNarvalAccounts(accounts: NarvalAccount[]) { + this.#accounts = accounts; + } + + async getAccounts() { + return this.#accounts.map((a) => a.address); + } + + removeAccount(address: string) { + const addressExists = this.#accounts + .map((a) => a.address.toLowerCase()) + .includes(address.toLowerCase()); + + if (!addressExists) { + throw new Error(`Address ${address} not found in this keyring`); + } + + this.#accounts = this.#accounts.filter( + (a) => a.address.toLowerCase() !== address.toLowerCase() + ); + } + + async signTransaction( + address: Hex, + tx: TypedTransaction, + opts: KeyringOpt = {} + ) { + if (!this.#armoryConfig) { + throw new Error('Narval not configured'); + } + + try { + const armoryClient = await getArmoryClient(this.#armoryConfig); + const armoryAccount = getArmoryAccount(armoryClient, address); + + const txData = tx.toJSON(); + const chainId = tx.common.chainIdBN().toNumber(); + + const transactionRequest = TransactionRequest.parse({ + ...txData, + chainId, + from: address, + gas: txData.gasLimit, + maxFeePerGas: txData.maxFeePerGas, + maxPriorityFeePerGas: txData.maxPriorityFeePerGas, + nonce: tx.nonce.toNumber(), + type: String(tx.type), + }); + + const serializedTransaction = await armoryAccount.signTransaction( + transactionRequest + ); + + const { r, s, v, yParity } = parseTransaction(serializedTransaction); + + const txDataWithSignature: JsonTx = { + chainId: transactionRequest.chainId + ? toHex(transactionRequest.chainId) + : undefined, + nonce: transactionRequest.nonce + ? toHex(transactionRequest.nonce) + : undefined, + to: transactionRequest.to!, + data: + !transactionRequest.data || transactionRequest.data === '0x' + ? undefined + : transactionRequest.data, + value: + !transactionRequest.value || transactionRequest.value === '0x' + ? undefined + : transactionRequest.value, + gasLimit: transactionRequest.gas + ? toHex(transactionRequest.gas) + : undefined, + type: transactionRequest.type + ? toHex(Number(transactionRequest.type)) + : undefined, + r, + s, + v: v ? toHex(v) : undefined, + }; + + if (transactionRequest.type === '2') { + txDataWithSignature.maxFeePerGas = transactionRequest.maxFeePerGas + ? toHex(transactionRequest.maxFeePerGas) + : undefined; + + txDataWithSignature.maxPriorityFeePerGas = transactionRequest.maxPriorityFeePerGas + ? toHex(transactionRequest.maxPriorityFeePerGas) + : undefined; + txDataWithSignature.v = yParity ? '0x1' : '0x0'; + } else { + txDataWithSignature.gasPrice = txData.gasPrice + ? toHex(txData.gasPrice) + : undefined; + } + + const buildTx = TransactionFactory.fromTxData(txDataWithSignature); + + return buildTx; + } catch (err) { + const message = + err?.context?.authorization?.errors.map((e) => e.message).join(', ') || + err?.message; + + throw new Error(message); + } + } + + // For personal_sign, we need to prefix the message: + async signPersonalMessage( + address: Hex, + rawMessage: Hex, + opts = { withAppKeyOrigin: '' } + ): Promise { + if (!this.#armoryConfig) { + throw new Error('Narval not configured'); + } + + try { + const armoryClient = await getArmoryClient(this.#armoryConfig); + const armoryAccount = getArmoryAccount(armoryClient, address); + + const signature = await armoryAccount.signMessage({ + message: { raw: rawMessage }, + }); + + return signature; + } catch (err) { + const message = + err?.context?.authorization?.errors.map((e) => e.message).join(', ') || + err?.message; + + throw new Error(message); + } + } + + // personal_signTypedData, signs data along with the schema + async signTypedData( + address: Hex, + typedData: Eip712TypedData, + opts: KeyringOpt = { version: SignTypedDataVersion.V1 } + ) { + if (!this.#armoryConfig) { + throw new Error('Narval not configured'); + } + + try { + // Treat invalid versions as "V1" + let version = SignTypedDataVersion.V1; + + if (opts.version && isSignTypedDataVersion(opts.version)) { + version = SignTypedDataVersion[opts.version]; + } + + const coercedTypedData = Eip712TypedData.parse(typedData); + const armoryClient = await getArmoryClient(this.#armoryConfig); + const armoryAccount = getArmoryAccount(armoryClient, address); + const signature = await armoryAccount.signTypedData(coercedTypedData); + + return signature; + } catch (err) { + const message = + err?.context?.authorization?.errors.map((e) => e.message).join(', ') || + err?.message; + + throw new Error(message); + } + } + + async signMessage() { + throw new Error('Not supported'); + } + + async decryptMessage(withAccount: any, encryptedData: any) { + throw new Error('Not supported'); + } + + async getEncryptionPublicKey(withAccount: Hex, opts?: KeyringOpt) { + throw new Error('Not supported'); + } + + async getAppKeyAddress(address: Hex, origin: string) { + throw new Error('Not supported'); + } + + async exportAccount(address: Hex, opts = { withAppKeyOrigin: '' }) { + throw new Error('Not supported'); + } +} + +/** + * Type predicate type guard to check if a string is in the enum SignTypedDataVersion. + * + * @param version - The string to check. + * @returns Whether it's in the enum. + */ +function isSignTypedDataVersion( + version: SignTypedDataVersion | string +): version is SignTypedDataVersion { + return version in SignTypedDataVersion; +} diff --git a/src/background/service/keyring/index.ts b/src/background/service/keyring/index.ts index 5b1514e8de8..4bb5e7a6e4a 100644 --- a/src/background/service/keyring/index.ts +++ b/src/background/service/keyring/index.ts @@ -27,6 +27,8 @@ import GnosisKeyring, { TransactionBuiltEvent, TransactionConfirmedEvent, } from './eth-gnosis-keyring'; +import NarvalKeyring, { NarvalAccount } from './eth-narval-keyring'; +import { ArmoryConfig } from '@/background/utils/armory'; import preference from '../preference'; import i18n from '../i18n'; import { KEYRING_TYPE, EVENTS, KEYRING_CLASS } from 'consts'; @@ -61,6 +63,7 @@ export const KEYRING_SDK_TYPES = { CoboArgusKeyring, CoinbaseKeyring, EthImKeyKeyring, + NarvalKeyring, }; interface MemStoreState { @@ -179,6 +182,116 @@ export class KeyringService extends EventEmitter { .then(() => keyring); } + getNarvalKeyringByConnectionId = async (connectionId: string) => { + const narvalKeyrings: NarvalKeyring[] = await this.getKeyringsByType( + KEYRING_CLASS.Narval + ); + + const keyring = narvalKeyrings.find((keyring) => { + const narvalConnectionId = keyring.getNarvalConnectionId(); + return narvalConnectionId === connectionId; + }); + + if (!keyring) { + throw new Error('Narval keyring not found'); + } + + return keyring; + }; + + async connectNarvalAccount( + armoryConfig: ArmoryConfig + ): Promise { + try { + const connectionId = NarvalKeyring.getNarvalConnectionId(armoryConfig); + const existingKeyring = await this.getNarvalKeyringByConnectionId( + connectionId + ); + + return existingKeyring; + } catch (error) { + let keyring: NarvalKeyring; + + return this.persistAllKeyrings() + .then( + this.addNewKeyring.bind(this, KEYRING_TYPE.NarvalKeyring, { + armoryConfig, + }) + ) + .then(async (_keyring) => { + keyring = _keyring; + return this.persistAllKeyrings.bind(this); + }) + .then(this.persistAllKeyrings.bind(this)) + .then(this.setUnlocked.bind(this)) + .then(this.fullUpdate.bind(this)) + .then(() => keyring); + } + } + + async selectNarvalAccounts(connectionId: string, accounts: NarvalAccount[]) { + const narvalKeyrings: NarvalKeyring[] = this.getKeyringsByType( + KEYRING_CLASS.Narval + ); + const _accounts = await Promise.all( + narvalKeyrings + .filter((k) => k.getNarvalConnectionId() !== connectionId) + .map((k) => k.getAccounts()) + ); + const allAccounts: string[] = _accounts + .reduce((m, n) => m.concat(n), [] as string[]) + .map((address) => normalizeAddress(address).toLowerCase()); + const isIncluded = accounts + .map(({ address }) => address) + .find((address) => { + return allAccounts.find( + (key) => + key === address.toLowerCase() || + key === ethUtil.stripHexPrefix(address) + ); + }); + if (isIncluded) { + const error = new Error( + JSON.stringify({ + address: isIncluded, + anchor: 'DuplicateAccountError', + }) + ); + return Promise.reject(error); + } + + const keyring = await this.getNarvalKeyringByConnectionId(connectionId); + keyring.setNarvalAccounts(accounts); + const [address] = await keyring.getAccounts(); + const allKeyrings = await this.getAllTypedAccounts(); + if (!contactBook.getContactByAddress(address)) { + const alias = generateAliasName({ + keyringType: KEYRING_TYPE.NarvalKeyring, + keyringCount: + allKeyrings.filter( + (keyring) => keyring.type === KEYRING_TYPE.NarvalKeyring + ).length - 1, + }); + contactBook.addAlias({ + address, + name: alias, + }); + } + + return this.persistAllKeyrings() + .then(() => this._updateMemStoreKeyrings()) + .then(() => this.fullUpdate()) + .then(() => keyring.getNarvalAccounts()); + } + + removeNarvalConnection(keyring: NarvalKeyring) { + this.keyrings = this.keyrings.filter((k) => k !== keyring); + + return this.persistAllKeyrings() + .then(() => this._updateMemStoreKeyrings()) + .then(() => this.fullUpdate()); + } + generateMnemonic(): string { return bip39.generateMnemonic(wordlist); } diff --git a/src/background/utils/armory.ts b/src/background/utils/armory.ts new file mode 100644 index 00000000000..e98135a7604 --- /dev/null +++ b/src/background/utils/armory.ts @@ -0,0 +1,188 @@ +import { v4 } from 'uuid'; +import { bytesToString, hexToString } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { + AuthClient, + AuthConfig, + VaultClient, + VaultConfig, + resourceId, + buildSignerEip191, + privateKeyToJwk, + Address, + Eip712TypedData, + Hex, + Request, + TransactionRequest, + Resource, + Permission, +} from '@narval-xyz/armory-sdk'; +import { + Action, + SignableMessage, +} from '@narval-xyz/armory-sdk/policy-engine-shared'; +import { + Alg, + SigningAlg, +} from '@narval-xyz/armory-sdk/policy-engine-shared/signature'; +import { NarvalAccount } from '../service/keyring/eth-narval-keyring'; +import { WalletDtoAccount } from '@narval-xyz/armory-sdk/src/lib/http/client/vault'; + +export type ArmoryConfig = { + credentialPrivateKey: Hex; + authHost: string; + authClientId: string; + vaultHost: string; + vaultClientId: string; +}; + +export type ArmoryConnection = { + connectionId: string; + credentialPublicKey: string; + accounts: NarvalAccount[]; +}; + +export type ArmoryClient = { + authClient: AuthClient; + vaultClient: VaultClient; +}; + +export const getArmoryClient = async ({ + credentialPrivateKey, + authHost, + authClientId, + vaultHost, + vaultClientId, +}: ArmoryConfig): Promise => { + const account = privateKeyToAccount(credentialPrivateKey); + + const credential = privateKeyToJwk( + credentialPrivateKey, + Alg.ES256K, + account.address + ); + + const signer = { + jwk: credential, + alg: SigningAlg.EIP191, + sign: await buildSignerEip191(credentialPrivateKey), + }; + + const authConfig: AuthConfig = { + host: authHost, + clientId: authClientId, + signer, + }; + + const vaultConfig: VaultConfig = { + host: vaultHost, + clientId: vaultClientId, + signer, + }; + + const authClient = new AuthClient(authConfig); + const vaultClient = new VaultClient(vaultConfig); + + return { + authClient, + vaultClient, + }; +}; + +export const getArmoryAccount = ( + { authClient, vaultClient }: ArmoryClient, + address: Address +) => { + return { + address, + + signMessage: async ({ + message, + }: { + message: SignableMessage; + }): Promise => { + // convert the msg to a string, temporarily b/c backend breaks otherwise + let msg = message; + + if (typeof message !== 'string') { + if (typeof message.raw === 'string') { + msg = hexToString(message.raw); + } else { + msg = bytesToString(message.raw); + } + } + + const request: Request = { + action: Action.SIGN_MESSAGE, + resourceId: resourceId(address), + message: msg, + nonce: v4(), + }; + + const accessToken = await authClient.requestAccessToken(request); + + const { signature } = await vaultClient.sign({ + data: request, + accessToken, + }); + + return signature; + }, + + signTransaction: async ( + transactionRequest: TransactionRequest + ): Promise => { + const request: Request = { + action: Action.SIGN_TRANSACTION, + resourceId: resourceId(address), + transactionRequest, + nonce: v4(), + }; + + const accessToken = await authClient.requestAccessToken(request); + + const { signature } = await vaultClient.sign({ + data: request, + accessToken, + }); + + return signature; + }, + + signTypedData: async (typedData: Eip712TypedData): Promise => { + const request: Request = { + action: Action.SIGN_TYPED_DATA, + resourceId: resourceId(address), + typedData, + nonce: v4(), + }; + + const accessToken = await authClient.requestAccessToken(request); + + const { signature } = await vaultClient.sign({ + data: request, + accessToken, + }); + + return signature; + }, + }; +}; + +export const listArmoryAccounts = async ({ + authClient, + vaultClient, +}: ArmoryClient): Promise => { + const request: Request = { + action: Action.GRANT_PERMISSION, + resourceId: Resource.VAULT, + nonce: v4(), + permissions: [Permission.WALLET_READ], + }; + + const accessToken = await authClient.requestAccessToken(request); + + const { accounts } = await vaultClient.listAccounts({ accessToken }); + + return accounts; +}; diff --git a/src/constant/index.ts b/src/constant/index.ts index 746d14ea76e..be2bec31bd2 100644 --- a/src/constant/index.ts +++ b/src/constant/index.ts @@ -183,6 +183,9 @@ import IconUtila, { import IconNgrave, { ReactComponent as RCIconNgrave, } from 'ui/assets/walletlogo/ngrave.svg'; +import IconNarval, { + ReactComponent as RCIconNarval, +} from 'ui/assets/walletlogo/narval.svg'; import { ensureChainHashValid, ensureChainListValid, @@ -239,6 +242,7 @@ export const KEYRING_TYPE = { GnosisKeyring: 'Gnosis', CoboArgusKeyring: 'CoboArgus', CoinbaseKeyring: 'Coinbase', + NarvalKeyring: 'Narval', } as const; export const KEYRING_CLASS = { @@ -258,6 +262,7 @@ export const KEYRING_CLASS = { GNOSIS: 'Gnosis', CoboArgus: 'CoboArgus', Coinbase: 'Coinbase', + Narval: 'Narval', } as const; export const KEYRING_WITH_INDEX = [ @@ -273,6 +278,7 @@ export const SUPPORT_1559_KEYRING_TYPE = [ KEYRING_CLASS.HARDWARE.KEYSTONE, KEYRING_CLASS.HARDWARE.TREZOR, KEYRING_CLASS.HARDWARE.ONEKEY, + KEYRING_CLASS.Narval, ]; export const KEYRING_TYPE_TEXT = { @@ -287,6 +293,7 @@ export const KEYRING_TYPE_TEXT = { [KEYRING_CLASS.GNOSIS]: 'Imported by Safe', [KEYRING_CLASS.HARDWARE.KEYSTONE]: 'Imported by QRCode Base', [KEYRING_CLASS.HARDWARE.IMKEY]: 'Imported by imKey', + [KEYRING_CLASS.Narval]: 'Imported by Narval', }; export const HARDWARE_KEYRING_TYPES = { @@ -437,6 +444,7 @@ export enum BRAND_WALLET_CONNECT_TYPE { CoboArgusConnect = 'CoboArgusConnect', CoinbaseConnect = 'CoinbaseConnect', ImKeyConnect = 'ImKeyConnect', + NarvalConnect = 'NarvalConnect', } export const WALLETCONNECT_STATUS_MAP = { @@ -534,6 +542,7 @@ export enum WALLET_BRAND_TYPES { IMKEY = 'IMKEY', NGRAVEZERO = 'NGRAVE ZERO', Utila = 'Utila', + NARVAL = 'Narval', } export enum WALLET_BRAND_CATEGORY { @@ -936,12 +945,25 @@ export const WALLET_BRAND_CONTENT: { connectType: BRAND_WALLET_CONNECT_TYPE.QRCodeBase, category: WALLET_BRAND_CATEGORY.HARDWARE, }, + [WALLET_BRAND_TYPES.NARVAL]: { + id: 32, + name: 'Narval', + brand: WALLET_BRAND_TYPES.NARVAL, + icon: IconNarval, + lightIcon: IconNarval, + image: IconNarval, + rcSvg: RCIconNarval, + maybeSvg: IconNarval, + connectType: BRAND_WALLET_CONNECT_TYPE.NarvalConnect, + category: WALLET_BRAND_CATEGORY.INSTITUTIONAL, + }, }; export const KEYRING_ICONS = { [KEYRING_CLASS.MNEMONIC]: IconMnemonicInk, [KEYRING_CLASS.PRIVATE_KEY]: IconPrivateKeyInk, [KEYRING_CLASS.WATCH]: IconWatchPurple, + [KEYRING_CLASS.Narval]: IconNarval, [HARDWARE_KEYRING_TYPES.BitBox02.type]: IconBitBox02, [HARDWARE_KEYRING_TYPES.Ledger.type]: LogoLedgerWhite, [HARDWARE_KEYRING_TYPES.Onekey.type]: LogoOnekey, @@ -967,6 +989,7 @@ export const KEYRING_ICONS_WHITE: Record = { [KEYRING_CLASS.MNEMONIC]: IconMnemonicWhite, [KEYRING_CLASS.PRIVATE_KEY]: IconPrivateKeyWhite, [KEYRING_CLASS.WATCH]: IconWatchWhite, + [KEYRING_CLASS.Narval]: IconNarval, [HARDWARE_KEYRING_TYPES.BitBox02.type]: IconBitBox02, [HARDWARE_KEYRING_TYPES.Ledger.type]: LogoLedgerWhite, [HARDWARE_KEYRING_TYPES.Onekey.type]: LogoOnekey, @@ -986,6 +1009,7 @@ export const KEYRINGS_LOGOS: Record = { [KEYRING_CLASS.MNEMONIC]: IconMnemonicWhite, [KEYRING_CLASS.PRIVATE_KEY]: LogoPrivateKey, [KEYRING_CLASS.WATCH]: IconWatchWhite, + [KEYRING_CLASS.Narval]: IconNarval, [HARDWARE_KEYRING_TYPES.BitBox02.type]: IconBitBox02WithBorder, [HARDWARE_KEYRING_TYPES.Ledger.type]: LogoLedgerWhite, [HARDWARE_KEYRING_TYPES.Onekey.type]: IconOneKey18, @@ -1418,6 +1442,7 @@ export const WALLET_SORT_SCORE = [ //institutional WALLET_BRAND_TYPES.GNOSIS, WALLET_BRAND_TYPES.CoboArgus, + WALLET_BRAND_TYPES.NARVAL, WALLET_BRAND_TYPES.AMBER, WALLET_BRAND_TYPES.FIREBLOCKS, WALLET_BRAND_TYPES.JADE, diff --git a/src/ui/assets/import/narval.svg b/src/ui/assets/import/narval.svg new file mode 100644 index 00000000000..0a088c3f75f --- /dev/null +++ b/src/ui/assets/import/narval.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/assets/walletlogo/narval.svg b/src/ui/assets/walletlogo/narval.svg new file mode 100644 index 00000000000..3578c63c89d --- /dev/null +++ b/src/ui/assets/walletlogo/narval.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/component/AddAddressOptions/index.tsx b/src/ui/component/AddAddressOptions/index.tsx index 2708ca3a6e2..8677177c570 100644 --- a/src/ui/component/AddAddressOptions/index.tsx +++ b/src/ui/component/AddAddressOptions/index.tsx @@ -174,6 +174,11 @@ const AddAddressOptions = () => { }); } else if (item.connectType === BRAND_WALLET_CONNECT_TYPE.ImKeyConnect) { openInternalPageInTab('import/hardware/imkey-connect'); + } else if (item.connectType === BRAND_WALLET_CONNECT_TYPE.NarvalConnect) { + history.push({ + pathname: '/import/narval', + state: params, + }); } else { history.push({ pathname: '/import/wallet-connect', diff --git a/src/ui/views/Approval/components/map.ts b/src/ui/views/Approval/components/map.ts index 8cdd2ad5dbf..b6abdb9e9ce 100644 --- a/src/ui/views/Approval/components/map.ts +++ b/src/ui/views/Approval/components/map.ts @@ -12,6 +12,7 @@ export const WaitingSignComponent = { [KEYRING_CLASS.PRIVATE_KEY]: 'PrivatekeyWaiting', [KEYRING_CLASS.Coinbase]: 'CoinbaseWaiting', [KEYRING_CLASS.HARDWARE.IMKEY]: 'ImKeyHardwareWaiting', + [KEYRING_CLASS.Narval]: 'PrivatekeyWaiting', }; export const WaitingSignMessageComponent = { @@ -26,4 +27,5 @@ export const WaitingSignMessageComponent = { [KEYRING_CLASS.HARDWARE.IMKEY]: 'ImKeyHardwareWaiting', [KEYRING_CLASS.MNEMONIC]: 'PrivatekeyWaiting', [KEYRING_CLASS.PRIVATE_KEY]: 'PrivatekeyWaiting', + [KEYRING_CLASS.Narval]: 'PrivatekeyWaiting', }; diff --git a/src/ui/views/ConnectNarval/NarvalAccountsList.tsx b/src/ui/views/ConnectNarval/NarvalAccountsList.tsx new file mode 100644 index 00000000000..1b992b11ec6 --- /dev/null +++ b/src/ui/views/ConnectNarval/NarvalAccountsList.tsx @@ -0,0 +1,163 @@ +import { + MultiSelectAddressList, + Navbar, + StrayPageWithButton, +} from '@/ui/component'; +import { useWallet, useWalletRequest } from '@/ui/utils'; +import React, { useEffect } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { useMedia } from 'react-use'; +import clsx from 'clsx'; +import { useTranslation } from 'react-i18next'; +import { KEYRING_CLASS } from '@/constant'; +import { useRepeatImportConfirm } from '../../utils/useRepeatImportConfirm'; +import { safeJSONParse } from '@/utils'; +import { NarvalAccount } from '@/background/service/keyring/eth-narval-keyring'; +import { Input } from 'antd'; +import IconSearch from 'ui/assets/search.svg'; +import './style.less'; + +export type AccountItem = { address: string; index: number }; + +const NarvalAccountsList = () => { + const history = useHistory(); + const wallet = useWallet(); + const isWide = useMedia('(min-width: 401px)'); + const { t } = useTranslation(); + const { show, contextHolder } = useRepeatImportConfirm(); + const { state } = useLocation<{ + connectionId: string; + accounts: NarvalAccount[]; + selectedAccounts: NarvalAccount[]; + }>(); + const [accounts, setAccounts] = React.useState([]); + const [filteredAccounts, setFilteredAccounts] = React.useState( + [] + ); + const [selectedAccounts, setSelectedAccounts] = React.useState( + [] + ); + const [searchKeyword, setSearchKeyword] = React.useState(''); + const [run, loading] = useWalletRequest(wallet.selectNarvalAccounts, { + async onSuccess(accounts) { + history.replace({ + pathname: '/popup/import/success', + state: { + accounts: accounts.map(({ address }, index) => { + return { + address, + index: index + 1, + type: KEYRING_CLASS.Narval, + alianName: `Narval Account #${index + 1}`, + }; + }), + title: t('page.newAddress.importedSuccessfully'), + editing: true, + importedAccount: true, + importedLength: accounts.length, + }, + }); + }, + onError(err) { + if (err?.message?.includes('DuplicateAccountError')) { + const address = safeJSONParse(err.message)?.address; + + show({ + address, + type: KEYRING_CLASS.Narval, + }); + } + }, + }); + + useEffect(() => { + const allAccounts = state.accounts.map((a, index) => ({ + address: a.address, + index: index + 1, + })); + + setAccounts(allAccounts); + setFilteredAccounts(allAccounts); + + const selectedAddresses = state.selectedAccounts.map((a) => a.address); + + setSelectedAccounts( + allAccounts.filter((a) => selectedAddresses.includes(a.address)) + ); + }, [state]); + + useEffect(() => { + if (!searchKeyword) { + setFilteredAccounts(accounts); + return; + } + + const keyword = searchKeyword.toLowerCase(); + + setFilteredAccounts( + accounts.filter((a) => a.address.toLowerCase().includes(keyword)) + ); + }, [accounts, searchKeyword]); + + const onBack = () => { + history.replace('/dashboard'); + }; + + const onNext = () => { + const selectedAddresses = selectedAccounts.map((a) => a.address); + + const filteredAccounts = state.accounts.filter((a) => + selectedAddresses.includes(a.address) + ); + + run(state.connectionId, filteredAccounts); + }; + + return ( + <> + {contextHolder} + + Select Accounts +
+
+
+ } + value={searchKeyword} + onChange={(e) => setSearchKeyword(e.target.value)} + autoFocus + /> +
+

{filteredAccounts.length} accounts found:

+ +
+
+
+
+
+ + ); +}; + +export default NarvalAccountsList; diff --git a/src/ui/views/ConnectNarval/NarvalConnectionForm.tsx b/src/ui/views/ConnectNarval/NarvalConnectionForm.tsx new file mode 100644 index 00000000000..6ee34cb8102 --- /dev/null +++ b/src/ui/views/ConnectNarval/NarvalConnectionForm.tsx @@ -0,0 +1,254 @@ +import React, { useCallback, useState } from 'react'; +import { Input, Form, message, Button } from 'antd'; +import { useHistory } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import IconSuccess from 'ui/assets/success.svg'; +import { Navbar, StrayPageWithButton } from 'ui/component'; +import { useWallet, useWalletRequest } from 'ui/utils'; +import { clearClipboard, copyTextToClipboard } from 'ui/utils/clipboard'; +import { useMedia } from 'react-use'; +import clsx from 'clsx'; +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; +import ThemeIcon from '@/ui/component/ThemeMode/ThemeIcon'; +import { ReactComponent as RcIconCopy } from 'ui/assets/component/icon-copy-cc.svg'; +import { unSuffix } from '../../../utils/string'; + +const NarvalConnectionForm = () => { + const history = useHistory(); + const wallet = useWallet(); + const [form] = Form.useForm(); + const { t } = useTranslation(); + const [credentialPrivateKey, setCredentialPrivateKey] = useState(''); + const [credentialAddress, setCredentialAddress] = useState(''); + const isWide = useMedia('(min-width: 401px)'); + + const [run, loading] = useWalletRequest(wallet.connectNarvalAccount, { + async onSuccess({ accounts, connectionId }) { + clearClipboard(); + + history.push({ + pathname: '/import/narval/accounts', + state: { accounts, connectionId, selectedAccounts: [] }, + }); + }, + async onError(err) { + if (['FORBIDDEN', 'FAILED'].includes(err?.message)) { + history.replace({ + pathname: '/import/narval/pending-permissions', + }); + return; + } + + if (err?.message?.includes('The private key is invalid')) { + form.setFields([ + { + name: 'credentialPrivateKey', + errors: [ + err?.message || + t('page.newAddress.privateKey.notAValidPrivateKey'), + ], + }, + ]); + } + }, + }); + + const onSubmit = (values: any) => { + run({ + ...values, + authHost: unSuffix(values.authHost), + vaultHost: unSuffix(values.vaultHost), + }); + }; + + const onCredentialAddressCopy = useCallback(() => { + copyTextToClipboard(credentialAddress).then(() => { + message.success({ + icon: , + content: t('global.copied'), + duration: 0.5, + }); + }); + }, [credentialAddress]); + + const onCredentialPrivateKeyCopy = useCallback(() => { + copyTextToClipboard(credentialPrivateKey).then(() => { + message.success({ + icon: , + content: t('global.copied'), + duration: 0.5, + }); + }); + }, [credentialPrivateKey]); + + const onPasteClear = () => { + clearClipboard(); + message.success({ + icon: , + content: t('page.newAddress.seedPhrase.pastedAndClear'), + duration: 2, + }); + }; + + const onBack = async () => { + history.replace('/dashboard'); + }; + + return ( + + New Connection +
+
+
+
+ + + + {credentialPrivateKey && ( + + )} + +
+ {credentialAddress && ( +
+
Credential Address:
+
+
{credentialAddress}
+ +
+
+ )} +
+ + + + + + + + + + + + + + + + +
+
+
+ ); +}; + +export default NarvalConnectionForm; diff --git a/src/ui/views/ConnectNarval/NarvalConnectionsList.tsx b/src/ui/views/ConnectNarval/NarvalConnectionsList.tsx new file mode 100644 index 00000000000..4d0912e629a --- /dev/null +++ b/src/ui/views/ConnectNarval/NarvalConnectionsList.tsx @@ -0,0 +1,151 @@ +import { ArmoryConnection } from '@/background/utils/armory'; +import { Navbar, StrayPageWithButton } from '@/ui/component'; +import { useWallet } from '@/ui/utils'; +import React, { useEffect, useState } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { useMedia } from 'react-use'; +import clsx from 'clsx'; +import { Button, message } from 'antd'; +import ThemeIcon from '@/ui/component/ThemeMode/ThemeIcon'; +import { ReactComponent as RcIconCopy } from 'ui/assets/component/icon-copy-cc.svg'; +import { ReactComponent as RcIconDelete } from 'ui/assets/address/delete.svg'; +import { copyTextToClipboard } from '@/ui/utils/clipboard'; +import { t } from 'i18next'; +import IconSuccess from 'ui/assets/success.svg'; + +export const formatAddress = ( + address?: string, + splitLength: number = 5 +): string => + address + ? `${address.substring(0, splitLength)}...${address.substring( + address.length - splitLength + )}` + : ''; + +const NarvalConnectionsList = () => { + const history = useHistory(); + const wallet = useWallet(); + const { state } = useLocation<{ connections: ArmoryConnection[] }>(); + const isWide = useMedia('(min-width: 401px)'); + const [connections, setConnections] = useState([]); + const [ + processingConnectionId, + setProcessingConnectionId, + ] = useState(); + + useEffect(() => { + setConnections(state.connections); + }, [state.connections]); + + const onBack = () => { + history.replace('/dashboard'); + }; + + const onNext = () => { + history.push({ + pathname: '/import/narval/connection-form', + }); + }; + + const onCopyAddress = (address) => { + copyTextToClipboard(address).then(() => { + message.success({ + icon: , + content: t('global.copied'), + duration: 0.5, + }); + }); + }; + + return ( + + Narval Connections +
+
+
    + {connections.map((connection) => { + return ( +
  • +
    + + {formatAddress(connection.credentialPublicKey)} + + + onCopyAddress(connection.credentialPublicKey) + } + /> +
    + {connection.accounts.length} Accounts + + { + const connections = await wallet.removeNarvalConnection( + connection.connectionId + ); + setConnections(connections); + if (!connections.length) { + history.replace('/import/narval'); + } + }} + /> +
  • + ); + })} +
+
+
+
+ ); +}; + +export default NarvalConnectionsList; diff --git a/src/ui/views/ConnectNarval/NarvalPendingPermissions.tsx b/src/ui/views/ConnectNarval/NarvalPendingPermissions.tsx new file mode 100644 index 00000000000..6b62db1faa8 --- /dev/null +++ b/src/ui/views/ConnectNarval/NarvalPendingPermissions.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { Navbar, StrayPageWithButton } from 'ui/component'; +import { useMedia } from 'react-use'; +import clsx from 'clsx'; + +const NarvalPendingPermissions = () => { + const history = useHistory(); + const isWide = useMedia('(min-width: 401px)'); + + const onBack = async () => { + history.replace({ + pathname: '/import/narval/', + }); + }; + + return ( + + Pending Permissions +
+
+
+

+ You don't have permissions to access the accounts of this client. +

+

+ It means someone needs to give your address permissions to access + the accounts. +

+

+ Please contact your organization administration or try again + later. +

+
+
+
+
+ ); +}; + +export default NarvalPendingPermissions; diff --git a/src/ui/views/ConnectNarval/index.tsx b/src/ui/views/ConnectNarval/index.tsx new file mode 100644 index 00000000000..a7ac5c1b924 --- /dev/null +++ b/src/ui/views/ConnectNarval/index.tsx @@ -0,0 +1,31 @@ +import { useWallet } from '@/ui/utils'; +import { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; + +const ConnectNarval = () => { + const history = useHistory(); + const wallet = useWallet(); + + useEffect(() => { + const init = async () => { + const connections = await wallet.getNarvalConnections(); + + if (!connections.length) { + history.push({ + pathname: '/import/narval/connection-form', + }); + } else { + history.push({ + pathname: '/import/narval/connections-list', + state: { connections }, + }); + } + }; + + init(); + }, []); + + return null; +}; + +export default ConnectNarval; diff --git a/src/ui/views/ConnectNarval/style.less b/src/ui/views/ConnectNarval/style.less new file mode 100644 index 00000000000..a1612a5bfc6 --- /dev/null +++ b/src/ui/views/ConnectNarval/style.less @@ -0,0 +1,11 @@ +.multiselect-address { + display: flex; + flex-direction: column; + align-items: normal; + + &__item { + &-index { + margin-right: 30px; + } + } +} \ No newline at end of file diff --git a/src/ui/views/MainRoute.tsx b/src/ui/views/MainRoute.tsx index 34fc90e7f5f..3534c4d6838 100644 --- a/src/ui/views/MainRoute.tsx +++ b/src/ui/views/MainRoute.tsx @@ -62,6 +62,11 @@ import { CustomTestnet } from './CustomTestnet'; import { AddFromCurrentSeedPhrase } from './AddFromCurrentSeedPhrase'; import { Ecology } from './Ecology'; import { Bridge } from './Bridge'; +import ConnectNarval from './ConnectNarval'; +import NarvalConnectionsList from './ConnectNarval/NarvalConnectionsList'; +import NarvalConnectionForm from './ConnectNarval/NarvalConnectionForm'; +import NarvalAccountsList from './ConnectNarval/NarvalAccountsList'; +import NarvalPendingPermissions from './ConnectNarval/NarvalPendingPermissions'; declare global { interface Window { @@ -123,6 +128,21 @@ const Main = () => { + + + + + + + + + + + + + + + diff --git a/yarn.lock b/yarn.lock index c3923225a30..2d9f48b63d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4207,6 +4207,20 @@ resolved "https://registry.yarnpkg.com/@mobily/ts-belt/-/ts-belt-3.13.1.tgz#8f8ce2a2eca41d88c2ca70c84d0f47d0f7f5cd5f" integrity sha512-K5KqIhPI/EoCTbA6CGbrenM9s41OouyK8A03fGJJcla/zKucsgLbz8HNbeseoLarRPgyWJsUyCYqFhI7t3Ra9Q== +"@narval-xyz/armory-sdk@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@narval-xyz/armory-sdk/-/armory-sdk-0.7.0.tgz#3e31e079dc2e9690eea91f8690c82971a5ef586c" + integrity sha512-Pzu1FcRhaO4i6dgfcBQSjvCDeYp+s/7CXbINZTS2BUpi+Lf2VsOCgCmYUuJc8dBY41g0t5rgyXufpZcmdjHPjQ== + dependencies: + "@noble/curves" "1.4.0" + axios "1.7.2" + jose "5.5.0" + lodash "4.17.21" + tslib "2.6.3" + uuid "9.0.1" + viem "2.16.2" + zod "3.23.8" + "@ngraveio/bc-ur@1.1.6", "@ngraveio/bc-ur@^1.1.5", "@ngraveio/bc-ur@^1.1.6": version "1.1.6" resolved "https://registry.yarnpkg.com/@ngraveio/bc-ur/-/bc-ur-1.1.6.tgz#8f8c75fff22f6a5e4dfbc5a6b540d7fe8f42cd39" @@ -4227,6 +4241,13 @@ dependencies: "@noble/hashes" "1.3.1" +"@noble/curves@1.2.0", "@noble/curves@^1.2.0", "@noble/curves@~1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" + integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== + dependencies: + "@noble/hashes" "1.3.2" + "@noble/curves@1.3.0", "@noble/curves@~1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.3.0.tgz#01be46da4fd195822dab821e72f71bf4aeec635e" @@ -4241,13 +4262,6 @@ dependencies: "@noble/hashes" "1.4.0" -"@noble/curves@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" - integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== - dependencies: - "@noble/hashes" "1.3.2" - "@noble/curves@~1.4.0": version "1.4.2" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" @@ -4888,16 +4902,16 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== +"@scure/base@~1.1.2", "@scure/base@~1.1.6": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.7.tgz#fe973311a5c6267846aa131bc72e96c5d40d2b30" + integrity sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g== + "@scure/base@~1.1.4": version "1.1.5" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.5.tgz#1d85d17269fe97694b9c592552dd9e5e33552157" integrity sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ== -"@scure/base@~1.1.6": - version "1.1.7" - resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.7.tgz#fe973311a5c6267846aa131bc72e96c5d40d2b30" - integrity sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g== - "@scure/bip32@1.1.5": version "1.1.5" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.5.tgz#d2ccae16dcc2e75bc1d75f5ef3c66a338d1ba300" @@ -4916,6 +4930,15 @@ "@noble/hashes" "~1.3.1" "@scure/base" "~1.1.0" +"@scure/bip32@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.2.tgz#90e78c027d5e30f0b22c1f8d50ff12f3fb7559f8" + integrity sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA== + dependencies: + "@noble/curves" "~1.2.0" + "@noble/hashes" "~1.3.2" + "@scure/base" "~1.1.2" + "@scure/bip32@1.3.3": version "1.3.3" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.3.tgz#a9624991dc8767087c57999a5d79488f48eae6c8" @@ -7209,6 +7232,11 @@ abab@^2.0.6: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== +abitype@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.4.tgz#a817ff44860e8a84e9a37ed22aa9b738dbb51dba" + integrity sha512-UivtYZOGJGE8rsrM/N5vdRkUpqEZVmuTumfTuolm7m/6O09wprd958rx8kUBwVAAAhQDveGAgD0GJdBuR8s6tw== + abitype@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.5.tgz#29d0daa3eea867ca90f7e4123144c1d1270774b6" @@ -7727,6 +7755,15 @@ axios@0.26.1: dependencies: follow-redirects "^1.14.8" +axios@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" + integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axios@^0.21.4: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" @@ -11074,6 +11111,11 @@ follow-redirects@^1.14.9: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -12876,6 +12918,11 @@ jiti@^1.21.0: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== +jose@5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/jose/-/jose-5.5.0.tgz#ec2a834606325d797851532733b14f1d5a40ee25" + integrity sha512-DUPr/1kYXbuqYpkCj9r66+B4SGCKXCLQ5ZbKCgmn4sJveJqcwNqWtAR56u4KPmpXjrmBO2uNuLdEAEiqIhFNBg== + js-cookie@^2.2.1, js-cookie@^2.x.x: version "2.2.1" resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" @@ -17863,6 +17910,11 @@ tslib@1.14.1, tslib@^1.10.0, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + tslib@^2.0.0, tslib@^2.3.0, tslib@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" @@ -18327,6 +18379,11 @@ uuid@9.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== +uuid@9.0.1, uuid@^9.0.0, uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + uuid@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" @@ -18342,11 +18399,6 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0.0, uuid@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== - uvu@^0.5.0: version "0.5.2" resolved "https://r.cnpmjs.org/uvu/download/uvu-0.5.2.tgz#c145e7f4b5becf80099cf22fd8a4a05f0112b2c0" @@ -18435,6 +18487,20 @@ victory-vendor@^36.6.8: d3-time "^3.0.0" d3-timer "^3.0.1" +viem@2.16.2: + version "2.16.2" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.16.2.tgz#227e14c61afc9057d2290501649e37f7ed0379be" + integrity sha512-qor3v1cJFR3jcPtcJxPbKfKURAH2agNf2IWZIaSReV6teNLERiu4Sr7kbqpkIeTAEpiDCVQwg336M+mub1m+pg== + dependencies: + "@adraffy/ens-normalize" "1.10.0" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@scure/bip32" "1.3.2" + "@scure/bip39" "1.2.1" + abitype "1.0.4" + isows "1.0.4" + ws "8.17.1" + viem@2.17.3: version "2.17.3" resolved "https://registry.yarnpkg.com/viem/-/viem-2.17.3.tgz#f15616049d8154b83e499eb5446e6d7fe6312626" @@ -19156,6 +19222,11 @@ zip-dir@2.0.0: async "^3.2.0" jszip "^3.2.2" +zod@3.23.8: + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== + zwitch@^2.0.0: version "2.0.2" resolved "https://r.cnpmjs.org/zwitch/download/zwitch-2.0.2.tgz#91f8d0e901ffa3d66599756dde7f57b17c95dce1"