diff --git a/packages/snap/package.json b/packages/snap/package.json index fb4e294..b3b9f36 100644 --- a/packages/snap/package.json +++ b/packages/snap/package.json @@ -1,6 +1,6 @@ { "name": "@cosmsnap/snap", - "version": "0.1.14", + "version": "0.1.15", "description": "The Cosmos extension for your Metamask wallet.", "repository": { "type": "git", diff --git a/packages/snap/snap.manifest.json b/packages/snap/snap.manifest.json index af995b1..467e82a 100644 --- a/packages/snap/snap.manifest.json +++ b/packages/snap/snap.manifest.json @@ -1,5 +1,5 @@ { - "version": "0.1.14", + "version": "0.1.15", "description": "Cosmos Extension that adds Cosmos support to Metamask.", "proposedName": "Cosmos Extension", "repository": { @@ -7,7 +7,7 @@ "url": "https://github.com/cosmos/snap.git" }, "source": { - "shasum": "OrlMarbyPhlqsFVD5Zm58D+uckRBrKujDszSqKIeAiE=", + "shasum": "9eVSJzLe5ny92sbfnBNnsq4Tgus0iU01NVTgpCDjeSo=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/snap/src/constants.ts b/packages/snap/src/constants.ts index 71d19ee..8b2a4fe 100644 --- a/packages/snap/src/constants.ts +++ b/packages/snap/src/constants.ts @@ -5,7 +5,7 @@ export const DEFAULT_FEES = { export const DEFAULT_SLIP44 = 118; -export const WALLET_URL = "https://wallet.mysticlabs.xyz"; +export const WALLET_URL = "https://metamask.mysticlabs.xyz"; // The multiplier to use to get u{denom} from {denom} export const U_MULTIPLIER = 1000000; diff --git a/packages/snap/src/index.ts b/packages/snap/src/index.ts index 2057a86..6ac60cd 100644 --- a/packages/snap/src/index.ts +++ b/packages/snap/src/index.ts @@ -11,6 +11,9 @@ import { COIN_TYPES, DEFAULT_FEES } from "./constants"; import { SignDoc, TxBody } from "cosmjs-types/cosmos/tx/v1beta1/tx"; import { StdSignDoc } from "@cosmjs/amino"; import { decodeProtoMessage } from "./parser"; +import Long from "long"; +import { Key } from '@keplr-wallet/types'; +import { fromBech32 } from '@cosmjs/encoding'; /** * Handle incoming JSON-RPC requests, sent through `wallet_invokeSnap`. @@ -244,7 +247,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ typeof request.params.chain_id == "string" ) ) { - throw new Error("Invalid transact request"); + throw new Error("Invalid sendTx request"); } let txBytes: Uint8Array = JSON.parse(request.params.tx) @@ -306,22 +309,19 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ typeof request.params == "object" && "sign_doc" in request.params && "chain_id" in request.params && - typeof request.params.chain_id == "string" + "signer" in request.params && + typeof request.params.chain_id == "string" && + typeof request.params.signer == "string" ) ) { - throw new Error("Invalid transact request"); - } - - let signer: string | null = null; - if (request.params.signer) { - if (typeof request.params.signer == "string") { - signer = request.params.signer - } + throw new Error("Invalid signDirect request"); } let signDoc: SignDoc = request.params.sign_doc as unknown as SignDoc; + let {low, high, unsigned} = signDoc.accountNumber + let accountNumber = new Long(low, high, unsigned); let signDocNew: SignDoc = { - accountNumber: signDoc.accountNumber, + accountNumber, bodyBytes: new Uint8Array(Object.values(signDoc.bodyBytes)), authInfoBytes: new Uint8Array(Object.values(signDoc.authInfoBytes)), chainId: signDoc.chainId @@ -362,10 +362,17 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ throw new Error("Transaction was denied."); } + let newSignDoc: SignDoc = { + bodyBytes: new Uint8Array(Object.values(signDoc.bodyBytes)), + authInfoBytes: new Uint8Array(Object.values(signDoc.authInfoBytes)), + chainId: signDoc.chainId, + accountNumber: new Long(signDoc.accountNumber.low, signDoc.accountNumber.high, signDoc.accountNumber.unsigned) + } + let resultTx = await signDirect( request.params.chain_id, - signer, - signDoc + request.params.signer, + newSignDoc ); if (typeof resultTx === "undefined") { @@ -389,10 +396,12 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ typeof request.params == "object" && "sign_doc" in request.params && "chain_id" in request.params && - typeof request.params.chain_id == "string" + "signer" in request.params && + typeof request.params.chain_id == "string" && + typeof request.params.signer == "string" ) ) { - throw new Error("Invalid transact request"); + throw new Error("Invalid signAmino request"); } let signerAmino: string | null = null; @@ -435,7 +444,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ let resultTxAmino = await signAmino( request.params.chain_id, - signerAmino, + request.params.signer, signDocAmino ); @@ -778,21 +787,89 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ typeof request.params.chain_id == "string" ) ) { - throw new Error("Invalid getChainAddress request"); + throw new Error("Invalid getAccountInfo request"); } let account: AccountData = await ChainState.GetAccount(request.params.chain_id); return { data: { - algo: account.algo.toString(), + algo: 'secp256k1', address: account.address, - pubkey: new Uint8Array(Object.values(account.pubkey)) + pubkey: new Uint8Array(Object.values(account.pubkey)), }, success: true, statusCode: 200, }; + case "getKey": + if ( + !( + request.params != null && + typeof request.params == "object" && + "chain_id" in request.params && + typeof request.params.chain_id == "string" + ) + ) { + throw new Error("Invalid getKey request"); + } + + let accountKey: AccountData = await ChainState.GetAccount(request.params.chain_id); + const bechInfo = fromBech32(accountKey.address); + const addressBytes = Uint8Array.from(bechInfo.data); + + let key: Key = { + algo: 'secp256k1', + address: addressBytes, + pubKey: new Uint8Array(Object.values(accountKey.pubkey)), + bech32Address: accountKey.address, + name: "Cosmos MetaMask Extension", + isNanoLedger: false, + isKeystone: false + } + + return { + data: key, + success: true, + statusCode: 200, + }; + + case "txAlert": + + if ( + !( + request.params != null && + typeof request.params == "object" && + "chain_id" in request.params && + "hash" in request.params && + typeof request.params.chain_id == "string" && + typeof request.params.hash == "string" + ) + ) { + throw new Error("Invalid txAlert request"); + } + + let hash: string = request.params.hash; + + await snap.request({ + method: "snap_dialog", + params: { + type: "alert", + content: panel([ + heading("Transaction Successful"), + text( + `Transaction with the hash ${hash} has been broadcasted to the chain ${request.params.chain_id}.` + ), + copyable(`${hash}`), + ]), + }, + }); + return { + data: {}, + success: true, + statusCode: 200, + }; + default: throw new Error("Method not found."); } diff --git a/packages/snap/src/transaction.ts b/packages/snap/src/transaction.ts index 73ce360..31562a7 100644 --- a/packages/snap/src/transaction.ts +++ b/packages/snap/src/transaction.ts @@ -1,7 +1,10 @@ import { DeliverTxResponse, SigningStargateClient } from "@cosmjs/stargate"; -import { DirectSecp256k1Wallet, DirectSignResponse } from "@cosmjs/proto-signing"; -import { Secp256k1Wallet, AminoSignResponse, StdSignDoc } from "@cosmjs/amino"; -import { Fees } from "./types/chains"; +import { encodeSecp256k1Signature, serializeSignDoc, rawSecp256k1PubkeyToRawAddress } from "@cosmjs/amino"; +import { Secp256k1, sha256 } from "@cosmjs/crypto"; +import { AccountData, DirectSecp256k1Wallet, DirectSignResponse, OfflineDirectSigner, makeSignBytes } from "@cosmjs/proto-signing"; +import { AminoSignResponse, StdSignDoc } from "@cosmjs/amino"; +import { toBech32 } from "@cosmjs/encoding"; +import { Chain, Fees } from "./types/chains"; import { ChainState } from "./state"; import { heading, panel, text } from "@metamask/snaps-ui"; import { @@ -11,6 +14,7 @@ import { DEFAULT_AVG_GAS, } from "./constants"; import { SignDoc } from "cosmjs-types/cosmos/tx/v1beta1/tx"; +import { getAddress } from "./address"; /** * submitTransaction Submits a transaction to the chain specified. @@ -192,9 +196,9 @@ export const sendTx = async ( */ export const signDirect = async ( chain_id: string, - signer: string | null, + signer: string, sign_doc: SignDoc -): Promise => { +): Promise => { try { // get the chain from state let chain = await ChainState.getChain(chain_id); @@ -221,22 +225,17 @@ export const signDirect = async ( if (pk.startsWith("0x")) { pk = pk.substring(2); } + // create Buffer for pk + let bytesPK = new Uint8Array(Buffer.from(pk, 'hex')); - // create the wallet - let wallet = await DirectSecp256k1Wallet.fromKey( - Uint8Array.from(Buffer.from(pk, "hex")), - chain.bech32_prefix - ); - - // get current wallet user as signer if signer not provided - if (signer == null) { - signer = (await wallet.getAccounts())[0].address; - } + let wallet = await Wallet.fromKey(bytesPK, chain); - // sign directly - let tx = await wallet.signDirect(signer, sign_doc); + let response = await wallet.signDirect(signer, sign_doc); - return tx + return { + signed: { ...sign_doc, accountNumber: sign_doc.accountNumber.toString() }, + signature: response.signature, + }; } catch (err: any) { console.error("Error During SignDirect: ", err.message); await snap.request({ @@ -264,7 +263,7 @@ export const signDirect = async ( */ export const signAmino = async ( chain_id: string, - signer: string | null, + signer: string, sign_doc: StdSignDoc ): Promise => { try { @@ -293,22 +292,14 @@ export const signAmino = async ( if (pk.startsWith("0x")) { pk = pk.substring(2); } + // create Buffer for pk + let bytesPK = new Uint8Array(Buffer.from(pk, 'hex')); - // create the wallet for Amino - let wallet = await Secp256k1Wallet.fromKey( - Uint8Array.from(Buffer.from(pk, "hex")), - chain.bech32_prefix - ); + let wallet = await Wallet.fromKey(bytesPK, chain); - // get current wallet user as signer if signer not provided - if (signer == null) { - signer = (await wallet.getAccounts())[0].address; - } + let response = await wallet.signAmino(signer, sign_doc); - // Sign using Amino - const signedTx = await wallet.signAmino(signer, sign_doc); // Adjust this based on your library - - return signedTx; + return response; } catch (err: any) { console.error("Error During signAmino: ", err.message); await snap.request({ @@ -322,4 +313,68 @@ export const signAmino = async ( }, }); } -}; \ No newline at end of file +}; + +export class Wallet implements OfflineDirectSigner { + /** + * Creates a DirectSecp256k1Wallet from the given private key + * + * @param privkey The private key. + * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". + */ + public static async fromKey(privkey: Uint8Array, chain: Chain): Promise { + const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey; + return new Wallet(privkey, Secp256k1.compressPubkey(uncompressed), chain); + } + + private readonly pubkey: Uint8Array; + private readonly privkey: Uint8Array; + private readonly chain: Chain; + + private constructor(privkey: Uint8Array, pubkey: Uint8Array, chain: Chain) { + this.privkey = privkey; + this.pubkey = pubkey; + this.chain = chain; + } + + public async getAccounts(): Promise { + let account = await getAddress(this.chain) + return [ + { + algo: "secp256k1", + address: account, + pubkey: this.pubkey, + }, + ]; + } + + public async signDirect(address: string, signDoc: SignDoc): Promise { + const signBytes = makeSignBytes(signDoc); + let checkAddress = await getAddress(this.chain); + if (address !== checkAddress) { + throw new Error(`Address ${address} not found in wallet`); + } + const hashedMessage = sha256(signBytes); + const signature = await Secp256k1.createSignature(hashedMessage, this.privkey); + const signatureBytes = new Uint8Array([...signature.r(32), ...signature.s(32)]); + const stdSignature = encodeSecp256k1Signature(this.pubkey, signatureBytes); + return { + signed: signDoc, + signature: stdSignature, + }; + } + + public async signAmino(signerAddress: string, signDoc: StdSignDoc): Promise { + let checkAddress = await getAddress(this.chain); + if (signerAddress !== checkAddress) { + throw new Error(`Address ${signerAddress} not found in wallet`); + } + const message = sha256(serializeSignDoc(signDoc)); + const signature = await Secp256k1.createSignature(message, this.privkey); + const signatureBytes = new Uint8Array([...signature.r(32), ...signature.s(32)]); + return { + signed: signDoc, + signature: encodeSecp256k1Signature(this.pubkey, signatureBytes), + }; + } +} \ No newline at end of file diff --git a/packages/snap/tests/address-book-test.ts b/packages/snap/tests/address-book-test.ts index e55c345..16ee48d 100644 --- a/packages/snap/tests/address-book-test.ts +++ b/packages/snap/tests/address-book-test.ts @@ -289,6 +289,6 @@ test.serial("AddressState Failing Tests", async (t) => { await failing_tests.getAddressFailTest( t, "5", - "5 is not found. Add the address to your address book at https://wallet.mysticlabs.xyz" + "5 is not found. Add the address to your address book at https://metamask.mysticlabs.xyz" ); }); diff --git a/packages/snapper/package.json b/packages/snapper/package.json index a32cc86..b933b86 100644 --- a/packages/snapper/package.json +++ b/packages/snapper/package.json @@ -1,6 +1,6 @@ { "name": "@cosmsnap/snapper", - "version": "0.1.21", + "version": "0.1.24", "description": "A helper package with utilities to interact with the Cosmos Extension for MetaMask.", "repository": { "type": "git", diff --git a/packages/snapper/src/provider.ts b/packages/snapper/src/provider.ts index f4c5691..7d907db 100644 --- a/packages/snapper/src/provider.ts +++ b/packages/snapper/src/provider.ts @@ -1,10 +1,10 @@ -import { AccountData, ChainInfo, OfflineAminoSigner, OfflineDirectSigner } from '@keplr-wallet/types'; +import { AccountData, ChainInfo, Key, OfflineAminoSigner, OfflineDirectSigner } from '@keplr-wallet/types'; import { DirectSignResponse } from "@cosmjs/proto-signing"; import { AminoSignResponse, StdSignDoc } from "@cosmjs/amino"; import { Long } from 'long'; import { Address, Chain, CosmosAddress, Fees, Msg } from './types'; import { DeliverTxResponse } from "@cosmjs/stargate"; -import { addAddressToBook, deleteAddressFromBook, deleteChain, getAccountInfo, getAddressBook, getBech32Address, getBech32Addresses, getChains, installSnap, isSnapInitialized, isSnapInstalled, sendTx, sign, signAmino, signAndBroadcast, signDirect, suggestChain } from './snap.js'; +import { DEFAULT_SNAP_ID, addAddressToBook, deleteAddressFromBook, deleteChain, getAccountInfo, getAddressBook, getBech32Address, getBech32Addresses, getChains, getKey, installSnap, isSnapInitialized, isSnapInstalled, sendTx, sign, signAmino, signAndBroadcast, signDirect, suggestChain } from './snap.js'; import { CosmJSOfflineSigner } from './signer.js'; import { SignDoc } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; @@ -16,7 +16,7 @@ declare global { } export interface SnapProvider { - experimentalSuggestChain(chainInfo: ChainInfo): Promise; + experimentalSuggestChain(chainInfo: ChainInfo): Promise; signAmino( chainId: string, signer: string, @@ -43,9 +43,9 @@ export interface SnapProvider { chainId: string, tx: Uint8Array, ): Promise; - getOfflineSigner(chainId: string): Promise; - enabled(): Promise; - install(): Promise; + getOfflineSigner(chainId: string): OfflineAminoSigner & OfflineDirectSigner; + getKey(chainId: string): Promise + enable(): Promise; getChains(): Promise; deleteChain(chain_id: string): Promise; signAndBroadcast(chain_id: string, msgs: Msg[], fees: Fees): Promise; @@ -60,7 +60,7 @@ export interface SnapProvider { } export class CosmosSnap implements SnapProvider { - snap_id: string; + snap_id: string = DEFAULT_SNAP_ID; changeSnapId(snap_id: string): void { this.snap_id = snap_id; } @@ -68,6 +68,10 @@ export class CosmosSnap implements SnapProvider { let account = await getAccountInfo(chain_id, this.snap_id); return account } + async getKey(chain_id: string): Promise { + let key = await getKey(chain_id, this.snap_id); + return key + } async deleteChain(chain_id: string): Promise { await deleteChain(chain_id, this.snap_id); } @@ -97,19 +101,20 @@ export class CosmosSnap implements SnapProvider { let address = getBech32Address(chain_id, this.snap_id); return address; } - async enabled(): Promise { - let installed = await isSnapInstalled(this.snap_id); - let initialized = await isSnapInitialized(this.snap_id); - return installed && initialized - } - async install(): Promise { + async enable(): Promise { await installSnap(this.snap_id); + return true; } async getChains(): Promise { let chains = await getChains(this.snap_id); return chains; } - async experimentalSuggestChain(chainInfo: ChainInfo): Promise { + async experimentalSuggestChain(chainInfo: ChainInfo): Promise { + let chains = await this.getChains(); + let chainIds = chains.map(item => item.chain_id); + if (chainIds.includes(chainInfo.chainId)) { + return true + } let chain: Chain = { pretty_name: chainInfo.chainName, chain_name: chainInfo.chainName, @@ -132,26 +137,25 @@ export class CosmosSnap implements SnapProvider { ] }, logo_URIs: { - png: chainInfo.chainSymbolImageUrl, - svg: chainInfo.chainSymbolImageUrl + png: chainInfo.chainSymbolImageUrl ?? undefined, + svg: chainInfo.chainSymbolImageUrl ?? undefined }, apis: { rpc: [ { address: chainInfo.rpc, - provider: chainInfo.nodeProvider.name } ], rest: [ { address: chainInfo.rest, - provider: chainInfo.nodeProvider.name } ] }, address: undefined } await suggestChain(chain, this.snap_id) + return true; } async signAmino(chainId: string, signer: string, signDoc: StdSignDoc): Promise { let res = await signAmino(chainId, signer, signDoc, this.snap_id); @@ -165,7 +169,7 @@ export class CosmosSnap implements SnapProvider { let res = await sendTx(chainId, tx, this.snap_id); return res } - async getOfflineSigner(chainId: string): Promise { + getOfflineSigner(chainId: string): OfflineAminoSigner & OfflineDirectSigner { return new CosmJSOfflineSigner(chainId, this.snap_id); } } \ No newline at end of file diff --git a/packages/snapper/src/signer.ts b/packages/snapper/src/signer.ts index a6d88f4..7093f90 100644 --- a/packages/snapper/src/signer.ts +++ b/packages/snapper/src/signer.ts @@ -1,7 +1,8 @@ import { SignDoc } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; import { AccountData, AminoSignResponse, StdSignDoc } from '@cosmjs/amino'; -import { DirectSignResponse, OfflineDirectSigner } from '@cosmjs/proto-signing'; +import { OfflineDirectSigner } from '@cosmjs/proto-signing'; import { getAccountInfo, signAmino, signDirect, DEFAULT_SNAP_ID } from './snap.js'; +import Long from 'long'; export class CosmJSOfflineSigner implements OfflineDirectSigner { readonly chainId: string; @@ -16,40 +17,53 @@ export class CosmJSOfflineSigner implements OfflineDirectSigner { } } - async getAccounts(): Promise { + public async getAccounts(): Promise { let address = await getAccountInfo(this.chainId, this.snapId); - return [ - { - address: address.address, - algo: address.algo, - pubkey: new Uint8Array(Object.values(address.pubkey)) - } + { + algo: "secp256k1", + address: address.address, + pubkey: new Uint8Array(Object.values(address.pubkey)), + }, ]; } - async signDirect( + public async signDirect( signerAddress: string, signDoc: SignDoc, - ): Promise { + ): Promise { if (this.chainId !== signDoc.chainId) { - throw new Error('Chain IDs do not match'); + throw new Error('Chain IDs do not match.'); } const accounts = await this.getAccounts(); let account = accounts[0]; if (account.address !== signerAddress) { - throw new Error('Signer address does not match wallet address'); + throw new Error('Signer address and wallet address do not match.'); } - let signRes = signDirect(this.chainId, account.address, signDoc, this.snapId); + let signRes = await signDirect(this.chainId, signerAddress, signDoc, this.snapId); - return signRes; + let { accountNumber } = signDoc; + + let newAN = new Long(accountNumber?.low || 0, accountNumber?.high, accountNumber?.unsigned); + + let sig = { + signature: signRes.signature, + signed: { + authInfoBytes: new Uint8Array(Object.values(signRes.signed.authInfoBytes)), + bodyBytes: new Uint8Array(Object.values(signRes.signed.bodyBytes)), + accountNumber: `${newAN.toString()}`, + ...signRes.signed, + }, + }; + + return sig; } // This has been added as a placeholder. - async signAmino( + public async signAmino( signerAddress: string, signDoc: StdSignDoc, ): Promise { @@ -62,10 +76,10 @@ export class CosmJSOfflineSigner implements OfflineDirectSigner { let account = accounts[0]; if (account.address !== signerAddress) { - throw new Error('Signer address does not match wallet address'); + throw new Error('Signer address and wallet address do not match.'); } - let signRes = signAmino(this.chainId, account.address, signDoc, this.snapId); + let signRes = await signAmino(this.chainId, account.address, signDoc, this.snapId); return signRes; } diff --git a/packages/snapper/src/snap.ts b/packages/snapper/src/snap.ts index 4c8a65a..25d4907 100644 --- a/packages/snapper/src/snap.ts +++ b/packages/snapper/src/snap.ts @@ -1,9 +1,10 @@ -import { Address, Chain, CosmosAddress, Fees, Msg } from './types'; +import { Address, Chain, CosmosAddress, Fees, Msg, SnapResponse } from './types'; import { StdSignDoc, AminoSignResponse } from "@cosmjs/amino"; import { DirectSignResponse } from '@cosmjs/proto-signing'; import { DeliverTxResponse } from "@cosmjs/stargate"; import { AccountData } from '@cosmjs/amino'; -import { SignDoc } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; +import Long from 'long'; +import { Key } from '@keplr-wallet/types'; export const DEFAULT_SNAP_ID = "npm:@cosmsnap/snap"; @@ -18,30 +19,29 @@ export const isSnapInitialized = async (snapId = DEFAULT_SNAP_ID): Promise { let installed = await isSnapInstalled(); - let initialized = await isSnapInitialized(); if (!installed) { await window.ethereum.request({ method: 'wallet_requestSnaps', params: { - snapId: { + [snapId]: { version: '^0.1.0', }, }, }); } - + let initialized = await isSnapInitialized(); if (!initialized) { await window.ethereum.request({ method: 'wallet_invokeSnap', @@ -230,6 +230,22 @@ export const getAccountInfo = async (chain_id: string, snapId = DEFAULT_SNAP_ID) return result.data; }; +export const getKey = async (chain_id: string, snapId = DEFAULT_SNAP_ID): Promise => { + const result = await window.ethereum.request({ + method: 'wallet_invokeSnap', + params: { + snapId, + request: { + method: 'getKey', + params: { + chain_id, + } + }, + }, + }); + return result.data; +}; + export const sendTx = async (chain_id: string, tx: Uint8Array, snapId = DEFAULT_SNAP_ID): Promise => { const result = await window.ethereum.request({ method: 'wallet_invokeSnap', @@ -250,10 +266,15 @@ export const sendTx = async (chain_id: string, tx: Uint8Array, snapId = DEFAULT_ export const signDirect = async ( chain_id: string, signer: string, - sign_doc: SignDoc, + sign_doc:{ + bodyBytes?: Uint8Array | null; + authInfoBytes?: Uint8Array | null; + chainId?: string | null; + accountNumber?: Long | null; + }, snapId = DEFAULT_SNAP_ID ): Promise => { - const result = await window.ethereum.request({ + const result: SnapResponse = await window.ethereum.request({ method: 'wallet_invokeSnap', params: { snapId, @@ -267,7 +288,7 @@ export const signDirect = async ( }, }, }); - return result.data; + return result.data }; export const signAmino = async ( @@ -291,4 +312,20 @@ export const signAmino = async ( }, }); return result.data; -}; \ No newline at end of file +}; + +export const sendTxAlert = async (chain_id: string, hash: string, snapId = DEFAULT_SNAP_ID): Promise => { + await window.ethereum.request({ + method: 'wallet_invokeSnap', + params: { + snapId, + request: { + method: 'txAlert', + params: { + chain_id, + hash + } + }, + }, + }); +}; diff --git a/packages/snapper/src/types.ts b/packages/snapper/src/types.ts index a135d5a..9e97ff7 100644 --- a/packages/snapper/src/types.ts +++ b/packages/snapper/src/types.ts @@ -89,4 +89,10 @@ export interface Address { export interface CosmosAddress { chain_id: string; address: string; +} + +export interface SnapResponse { + data: T; + success: boolean; + statusCode: number; } \ No newline at end of file diff --git a/packages/ui/package.json b/packages/ui/package.json index 91352cd..f4f4e0c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -37,7 +37,7 @@ "dependencies": { "@cosmjs/crypto": "^0.31.1", "@cosmjs/stargate": "^0.31.1", - "@cosmsnap/snapper": "^0.1.21", + "@cosmsnap/snapper": "0.1.24", "@keplr-wallet/types": "0.12.12", "@metamask/detect-provider": "^2.0.0", "@sveltejs/adapter-node": "^1.3.1", diff --git a/packages/ui/src/app.html b/packages/ui/src/app.html index 8ff291c..c4aeb5d 100644 --- a/packages/ui/src/app.html +++ b/packages/ui/src/app.html @@ -5,6 +5,7 @@ Cosmos MetaMask Extension + @@ -13,5 +14,9 @@
%sveltekit.body%
+ + \ No newline at end of file diff --git a/packages/ui/src/components/AddAddress.svelte b/packages/ui/src/components/AddAddress.svelte index 6fef609..8413edd 100644 --- a/packages/ui/src/components/AddAddress.svelte +++ b/packages/ui/src/components/AddAddress.svelte @@ -10,12 +10,13 @@ let address = "cosmos163gulek3trdckcktcv820dpxntnm7qkkgfkcga"; let name = "John Doe"; let loading = false; + export let open = false; const addAddress = async () => { try { loading = true; await addAddressToBook(chain_id, address, name); - $state.openAddAddressPopup = false; + open = false; await getAddressBook(); loading = false; } catch (err) { @@ -29,7 +30,7 @@ } -
+ -
{$state.alertText}
+
{$state.alertText}
-
{$state.alertText}
+
{$state.alertText}
@@ -107,4 +107,10 @@ font-style: normal; font-weight: 500; } + + @media (max-width: 900px) { + .navbar { + padding: 24px 10px; + } + } \ No newline at end of file diff --git a/packages/ui/src/components/MainTitle.svelte b/packages/ui/src/components/MainTitle.svelte index a77855f..549669e 100644 --- a/packages/ui/src/components/MainTitle.svelte +++ b/packages/ui/src/components/MainTitle.svelte @@ -28,7 +28,6 @@ margin-bottom: 60px; min-height: 136px; text-align: center; - width: 731px; } .span1 { diff --git a/packages/ui/src/components/Menu.svelte b/packages/ui/src/components/Menu.svelte index c3abf36..274afc8 100644 --- a/packages/ui/src/components/Menu.svelte +++ b/packages/ui/src/components/Menu.svelte @@ -3,10 +3,12 @@ import { page } from '$app/stores'; let menu_items = [ - { route: "/balances", title: "Balances", icon: "https://anima-uploads.s3.amazonaws.com/projects/64863aebc1255e7dd4fb600b/releases/64a70dda287bc6479f0ac9fd/img/dashboard.svg", path: "/balances" }, - { route: "/transactions", title: "History", icon: "https://anima-uploads.s3.amazonaws.com/projects/64863aebc1255e7dd4fb600b/releases/64a70dda287bc6479f0ac9fd/img/dns.svg", path: "/transactions" }, - { route: "/address", title: "Address Book", icon: "https://anima-uploads.s3.amazonaws.com/projects/64863aebc1255e7dd4fb600b/releases/64a70dda287bc6479f0ac9fd/img/account-box.svg", path: "/address" }, - { route: "/settings", title: "Settings", icon: "https://anima-uploads.s3.amazonaws.com/projects/64863aebc1255e7dd4fb600b/releases/64a70dda287bc6479f0ac9fd/img/settings.svg", path: "/settings" } + { route: "/balances", title: "Balances", icon: "M11.6 16.733c.234.268.548.456.895.534a1.4 1.4 0 0 0 1.75-.762c.172-.615-.445-1.287-1.242-1.481-.796-.194-1.41-.862-1.241-1.481a1.4 1.4 0 0 1 1.75-.763c.343.078.654.261.888.525m-1.358 4.017v.617m0-5.94v.726M1 10l5-4 4 1 7-6m0 0h-3.207M17 1v3.207M5 19v-6m-4 6v-4m17 0a5 5 0 1 1-10 0 5 5 0 0 1 10 0Z", path: "/balances" }, + { route: "/transactions", title: "History", icon: "M1 1h14M1 6h14M1 11h7", path: "/transactions" }, + { route: "/address", title: "Address Book", icon: "M1 17V2a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H3a2 2 0 0 0-2 2Zm0 0a2 2 0 0 0 2 2h12M5 15V1m8 18v-4", path: "/address" }, + { route: "/swap", title: "Swap", icon: "M16 1v5h-5M2 19v-5h5m10-4a8 8 0 0 1-14.947 3.97M1 10a8 8 0 0 1 14.947-3.97", path: "/swap" }, + { route: "/fiat", title: "Fiat", icon: "M4 9h2m3 0h5M1 5h18M2 1h16a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1Z", path: "/fiat" }, + { route: "/settings", title: "Settings", icon: "M19 11V9a1 1 0 0 0-1-1h-.757l-.707-1.707.535-.536a1 1 0 0 0 0-1.414l-1.414-1.414a1 1 0 0 0-1.414 0l-.536.535L12 2.757V2a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v.757l-1.707.707-.536-.535a1 1 0 0 0-1.414 0L2.929 4.343a1 1 0 0 0 0 1.414l.536.536L2.757 8H2a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h.757l.707 1.707-.535.536a1 1 0 0 0 0 1.414l1.414 1.414a1 1 0 0 0 1.414 0l.536-.535L8 17.243V18a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-.757l1.707-.708.536.536a1 1 0 0 0 1.414 0l1.414-1.414a1 1 0 0 0 0-1.414l-.535-.536.707-1.707H18a1 1 0 0 0 1-1Z", path: "/settings" } ] let active = $page.route.id; @@ -21,11 +23,9 @@
goto(tab.path)} class="w-full flex justify-between h-[40px] items-center cursor-pointer">
- {tab.title.toLowerCase()} +
{tab.title}
@@ -63,8 +63,8 @@ } .item { - height: 20px; - min-width: 20px; + height: 18px; + min-width: 18px; position: relative; } diff --git a/packages/ui/src/components/Step.svelte b/packages/ui/src/components/Step.svelte index c0f9ea3..050c95d 100644 --- a/packages/ui/src/components/Step.svelte +++ b/packages/ui/src/components/Step.svelte @@ -1,4 +1,6 @@
@@ -29,6 +32,7 @@ {/if}
+
- +
@@ -81,28 +83,6 @@ font-weight: 400; } - .frame-1-4 { - align-items: center; - background-color: var(--blueberry); - border-radius: 10px; - display: flex; - gap: 10px; - justify-content: center; - margin-top: 15px; - overflow: hidden; - padding: 10px 16px; - position: relative; - width: fit-content; - } - - .inter-medium-white-12px { - color: var(--white); - font-family: var(--font-family-inter); - font-size: var(--font-size-s); - font-style: normal; - font-weight: 500; - } - .step { letter-spacing: 2.1px; line-height: normal; @@ -210,44 +190,6 @@ text-align: center; } - .frame-1-2 { - margin-right: 2px; - } - - .frame-1-4 { - align-items: center; - background-color: var(--blueberry); - border-radius: 10px; - display: flex; - gap: 10px; - justify-content: center; - margin-top: 35px; - overflow: hidden; - padding: 10px 16px; - position: relative; - width: fit-content; - } - - .sign-in-meta-mask { - letter-spacing: -0.36px; - line-height: normal; - margin-top: 15px; - position: relative; - width: fit-content; - height: 35px; - min-width: 100px; - } - - .sign-in-meta-mask:hover { - background-color: var(--blueberry); - filter: brightness(1.1); - } - - .sign-in-meta-mask:disabled { - background-color: var(--blueberry); - opacity: 0.50; - } - .group-10-1 { align-items: center; display: flex; @@ -307,12 +249,6 @@ font-size: var(--responsive-font-size); } - .frame-1-4 { - gap: var(--responsive-gap); - padding: var(--responsive-padding); - margin-top: var(--responsive-margin); - } - .flex-row-1 { gap: 100px; /* reduced gap for smaller screens */ } @@ -342,27 +278,8 @@ font-size: var(--responsive-font-size); } - .frame-1-4 { - gap: var(--responsive-gap); - padding: var(--responsive-padding); - margin-top: var(--responsive-margin); - } - .flex-row-1 { gap: 50px; /* further reduced gap for mobile screens */ } - - .overlap-group1 { - width: 260px; /* further reduced width */ - } - - .group-9-3 { - width: 220px; /* further reduced width */ - } - - .sign-in-meta-mask { - height: 35px; - min-width: 100px; - } } \ No newline at end of file diff --git a/packages/ui/src/components/Transfer.svelte b/packages/ui/src/components/Transfer.svelte index 36ffaba..c421e89 100644 --- a/packages/ui/src/components/Transfer.svelte +++ b/packages/ui/src/components/Transfer.svelte @@ -10,25 +10,30 @@ import { addTransaction } from "../store/transactions"; import Button from "./Button.svelte"; import ChainSelector from "./ChainSelector.svelte"; + import { sendTxAlert } from "@cosmsnap/snapper"; import Select from "./Select.svelte"; - + let loading = false; let source = "cosmoshub-4"; let destination = "cosmoshub-4"; - let selected: CoinIBC = {amount: "0", denom: "uatom", ibc: false, display: "uatom".substring(1).toUpperCase()}; + let selected: any; + let sourceChainChange = false; let amount = 0; let noRoute = false; let recipient = ""; let slippage = "1"; let sourceBalances: CoinIBC[] = []; + let feesAmount = 0.25; + let gas = 0.25; + let feesOpen = false; let fees = { amount: [ { - amount: "100000", + amount: feesAmount.toString(), denom: "" } ], - gas: "5000", + gas: gas.toString(), }; let fromAddress: string | undefined = ""; let fromChain: Chain = { @@ -47,40 +52,37 @@ }; $: { - if (!selected) { - selected = {amount: "0", denom: "uatom", ibc: false, display: "uatom".substring(1).toUpperCase()}; - } - if (typeof amount != "number") { - amount = 0 - } - if (!fees || !fees.amount) { + if (feesAmount) { fees = { amount: [ { - amount: "100000", + amount: (feesAmount * 1000000).toString(), denom: "" } ], - gas: "5000", + gas: (gas * 1000000).toString(), } } - if (source !== fromChain.chain_id) { - let foundChain = $chains.find(item => item.chain_id === source); - if (foundChain) { - fromChain = foundChain; - fees.amount[0].denom = fromChain.fees.fee_tokens[0].denom; - fees.gas = (fromChain.fees.fee_tokens[0].average_gas_price * 1000000).toString(); - } + let foundChain = $chains.find(item => item.chain_id === source); + if (foundChain) { + fromChain = foundChain; + fees.amount[0].denom = fromChain.fees.fee_tokens[0].denom; if ($balances) { let source_chain = $balances.filter(item => item.chain_id == source)[0]; if (source_chain) { sourceBalances = source_chain.balances; - selected = sourceBalances[0]; + if(sourceChainChange) { + selected = sourceBalances[0]; + sourceChainChange = false; + } } } } - fromAddress = fromChain.address + fromAddress = fromChain.address; + if (!selected) { + selected = {amount: "0", denom: "uatom", ibc: false, display: "uatom".substring(1).toUpperCase()}; + } } const computeIBCRoute = async () => { @@ -100,15 +102,24 @@ amount: (amount * 1000000).toString(), }, ] - const tx = await client.sendTokens(fromAddress, recipient, coins, fees); + let msg = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: { + fromAddress, + toAddress: recipient, + amount: coins + } + } + const tx = await client.signAndBroadcast(fromAddress, [msg], fees); if (tx.code == 0) { - await addTransaction({address: fromAddress, chain: source, when: new Date().toDateString(), tx_hash: tx.transactionHash}) + await addTransaction({address: fromAddress, chain: source, when: new Date().toLocaleString(), tx_hash: tx.transactionHash}); + await sendTxAlert(source, tx.transactionHash); } else { if (tx.rawLog) { $state.alertText = tx.rawLog } else { - $state.alertText = "There was an issue while submitting your transaction. View explorer for more details." + $state.alertText = "There was an issue while submitting your transaction." } $state.alertType = "danger" $state.showAlert = true @@ -149,10 +160,11 @@ typeUrl: item.msg_type_url }; }); - let tx = await window.cosmos.signAndBroadcast(fromAddress, messages, fees); + const tx = await client.signAndBroadcast(fromAddress, messages, fees); if (tx.code == 0) { - await addTransaction({address: fromAddress, chain: source, when: new Date().toDateString(), tx_hash: tx.transactionHash}) + await addTransaction({address: fromAddress, chain: source, when: new Date().toDateString(), tx_hash: tx.transactionHash}); + await sendTxAlert(source, tx.transactionHash); } else { if (tx.rawLog) { $state.alertText = tx.rawLog @@ -176,15 +188,20 @@
-
- {source == destination ? "Transfer" : "IBC Transfer"} +
+
+ {source == destination ? "Transfer" : "IBC Transfer"} +
+ feesOpen = !feesOpen} class="w-4 h-4 text-white cursor-pointer" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"> + +
Source Chain
- + sourceChainChange = true} bind:selectedChain={source}/>
Asset @@ -192,6 +209,10 @@ + +
{ amount = _.round((Number(selected.amount) / 1000000)) }} class="available-balance-1454789 inter-medium-blueberry-14px cursor-pointer"> + Available: {_.round((Number(selected.amount) / 1000000))} {selected.display} +
Destination Chain @@ -205,9 +226,25 @@ Route Not Found
- -
{ amount = _.round((Number(selected.amount) / 1000000)) }} class="available-balance-1454789 inter-medium-blueberry-14px cursor-pointer"> - Available: {_.round((Number(selected.amount) / 1000000))} {selected.display} +
diff --git a/packages/ui/src/routes/+layout.svelte b/packages/ui/src/routes/+layout.svelte index 9bbe220..7e42f30 100644 --- a/packages/ui/src/routes/+layout.svelte +++ b/packages/ui/src/routes/+layout.svelte @@ -7,13 +7,44 @@ import Alert from "../components/Alert.svelte"; import Menu from "../components/Menu.svelte"; import { updateDirectory } from "../store/directory"; - import { CosmosSnap } from "@cosmsnap/snapper"; - import { snapId } from "../utils/snap"; + import { CosmosSnap, isSnapInitialized, isSnapInstalled } from "@cosmsnap/snapper"; + import { isMetaMaskInstalled, snapId } from "../utils/snap"; + + $: { + if ($state.isMetaMaskInstalledValue && $state.isSnapInitValue && $state.isSnapInstalledValue) { + $state.connected = true; + goto("/balances"); + } + } + + const initializeData = async () => { + try { + $state.loading = true; + $state.isMetaMaskInstalledValue = isMetaMaskInstalled() ?? false; + $state.loading = false; + if ($state.isSnapInstalledValue) { + $state.loading = true; + $state.isSnapInitValue = await isSnapInitialized(); + $state.loading = false; + } + if ($state.isMetaMaskInstalledValue) { + $state.loading = true; + $state.isSnapInstalledValue = await isSnapInstalled(); + $state.loading = false; + } + } catch (err: any) { + $state.loading = false; + $state.alertText = `${err.message}` + $state.alertType = "danger" + $state.showAlert = true + } + }; onMount(async () => { window.cosmos = new CosmosSnap(); window.cosmos.changeSnapId(snapId); updateDirectory(); + initializeData(); if (!$state.connected) { goto("/"); } else { @@ -80,7 +111,7 @@ .left-content { backdrop-filter: blur(15px) brightness(100%); - background-color: var(--black); + background-color: #05000b; border: 1px solid; border-color: var(--white-2); border-top: 0px; diff --git a/packages/ui/src/routes/+page.svelte b/packages/ui/src/routes/+page.svelte index 03581fd..1dec1b8 100644 --- a/packages/ui/src/routes/+page.svelte +++ b/packages/ui/src/routes/+page.svelte @@ -1,47 +1,52 @@
@@ -52,9 +57,10 @@
{ window.open('https://metamask.io/download', '_blank') }} - complete={isMetaMaskInstalledValue} + complete={$state.isMetaMaskInstalledValue} stepNumber="1" stepTitle="Install Metamask" stepImage="https://anima-uploads.s3.amazonaws.com/projects/64863aebc1255e7dd4fb600b/releases/64863c03ac0993f6e77c817f/img/metamask-1.svg" @@ -65,22 +71,24 @@ /> { await initializeSnap(); }} - complete={isSnapInitValue} + complete={$state.isSnapInitValue} stepNumber="3" stepTitle="Initiate Cosmos Snap" stepImage="/cosmos-atom-logo.png" @@ -151,6 +159,7 @@ min-height: 17px; min-width: 160px; text-align: center; + margin-bottom: 20px; } /* Responsive Styles */ @@ -181,7 +190,6 @@ min-height: 17px; min-width: 160px; text-align: center; - margin-bottom: 20px; } } \ No newline at end of file diff --git a/packages/ui/src/routes/address/+page.svelte b/packages/ui/src/routes/address/+page.svelte index ade317c..2b6728b 100644 --- a/packages/ui/src/routes/address/+page.svelte +++ b/packages/ui/src/routes/address/+page.svelte @@ -11,6 +11,7 @@ let searchResults: lunr.Index.Result[] = []; let currentAddresses: Address[] = $addressbook; let copied = false; + let open = false; const idx = lunr(function () { // Use this ref function to get the id that will refer to each document @@ -52,13 +53,13 @@
-