From 698cb5f85cbec7bc7f66dc3c62ef4ceffa5d0e1a Mon Sep 17 00:00:00 2001 From: baryon2 Date: Tue, 25 Jul 2023 16:21:44 +0200 Subject: [PATCH 1/2] Add snap provider package --- .github/workflows/publish-provider.yml | 26 ++++ .gitignore | 3 +- package.json | 1 + packages/cosmos-snap-provider/README.md | 1 + packages/cosmos-snap-provider/package.json | 18 +++ .../cosmos-snap-provider/src/app-env.d.ts | 7 + packages/cosmos-snap-provider/src/config.ts | 2 + .../src/cosmjs-offline-signer.ts | 54 +++++++ packages/cosmos-snap-provider/src/index.ts | 3 + packages/cosmos-snap-provider/src/snap.ts | 133 ++++++++++++++++++ packages/cosmos-snap-provider/src/types.ts | 8 ++ packages/cosmos-snap-provider/tsconfig.json | 8 ++ packages/snap/snap.manifest.json | 4 +- yarn.lock | 6 + 14 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/publish-provider.yml create mode 100644 packages/cosmos-snap-provider/README.md create mode 100644 packages/cosmos-snap-provider/package.json create mode 100644 packages/cosmos-snap-provider/src/app-env.d.ts create mode 100644 packages/cosmos-snap-provider/src/config.ts create mode 100644 packages/cosmos-snap-provider/src/cosmjs-offline-signer.ts create mode 100644 packages/cosmos-snap-provider/src/index.ts create mode 100644 packages/cosmos-snap-provider/src/snap.ts create mode 100644 packages/cosmos-snap-provider/src/types.ts create mode 100644 packages/cosmos-snap-provider/tsconfig.json diff --git a/.github/workflows/publish-provider.yml b/.github/workflows/publish-provider.yml new file mode 100644 index 0000000..3bc1aaa --- /dev/null +++ b/.github/workflows/publish-provider.yml @@ -0,0 +1,26 @@ +on: + push: + branches: + - release + paths: + - 'packages/comos-snap-provider' +jobs: + release: + runs-on: ubuntu-latest + env: + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} + GH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2.4.1 + with: + node-version: '16.13.0' + registry-url: 'https://npm.pkg.github.com' + scope: '@leapwallet' + + - name: Build Provider + run: yarn build:provider + + - name: Bump version and publish package + run: + yarn publish --non-interactive diff --git a/.gitignore b/.gitignore index 768e9d3..d809294 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ dist/ build/ coverage/ .cache/ +dist/ # Logs logs @@ -77,4 +78,4 @@ node_modules/ .yarnrc.yml -.npmrc \ No newline at end of file +.npmrc diff --git a/package.json b/package.json index 9956238..724aa5a 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "lint:misc": "prettier '**/*.json' '**/*.md' '!**/CHANGELOG.md' '**/*.yml' --ignore-path .gitignore", "start": "yarn workspaces foreach --parallel --interlaced --verbose run start", "start:snap": "yarn workspace snap start", + "build:provider": "yarn workspace @leapwallet/cosmos-snap-provider build", "test": "echo \"TODO\"" }, "devDependencies": { diff --git a/packages/cosmos-snap-provider/README.md b/packages/cosmos-snap-provider/README.md new file mode 100644 index 0000000..03693e4 --- /dev/null +++ b/packages/cosmos-snap-provider/README.md @@ -0,0 +1 @@ +# cosmos-snap-provider diff --git a/packages/cosmos-snap-provider/package.json b/packages/cosmos-snap-provider/package.json new file mode 100644 index 0000000..c19ab11 --- /dev/null +++ b/packages/cosmos-snap-provider/package.json @@ -0,0 +1,18 @@ +{ + "name": "@leapwallet/cosmos-snap-provider", + "packageManager": "yarn@3.2.1", + "files": [ + "dist/**/*" + ], + "repository": { + "url": "git@github.com:leapwallet/cosmos-metamask-snap.git" + }, + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, + "scripts": { + "build": "npx tsc", + "start": "npx tsc watch", + "prepublish": "yarn build" + } +} diff --git a/packages/cosmos-snap-provider/src/app-env.d.ts b/packages/cosmos-snap-provider/src/app-env.d.ts new file mode 100644 index 0000000..5b35708 --- /dev/null +++ b/packages/cosmos-snap-provider/src/app-env.d.ts @@ -0,0 +1,7 @@ +export {}; + +declare global { + interface Window { + ethereum: any; + } +} diff --git a/packages/cosmos-snap-provider/src/config.ts b/packages/cosmos-snap-provider/src/config.ts new file mode 100644 index 0000000..e5f0055 --- /dev/null +++ b/packages/cosmos-snap-provider/src/config.ts @@ -0,0 +1,2 @@ +export const defaultSnapOrigin = + process.env.SNAP_ORIGIN ?? `npm:@leapwallet/metamask-cosmos-snap`; diff --git a/packages/cosmos-snap-provider/src/cosmjs-offline-signer.ts b/packages/cosmos-snap-provider/src/cosmjs-offline-signer.ts new file mode 100644 index 0000000..07292f6 --- /dev/null +++ b/packages/cosmos-snap-provider/src/cosmjs-offline-signer.ts @@ -0,0 +1,54 @@ +import { SignDoc } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; +import { AccountData, AminoSignResponse } from '@cosmjs/amino'; +import { getKey, requestSignature } from './snap'; +import { DirectSignResponse, OfflineDirectSigner } from '@cosmjs/proto-signing'; + +export class cosmjsOfflineSigner implements OfflineDirectSigner { + constructor(private chainId: string) {} + + async getAccounts(): Promise { + const key = await getKey(this.chainId); + return [ + { + address: key.address, + algo: 'secp256k1', + pubkey: key.pubkey, + }, + ]; + } + + async signDirect( + signerAddress: string, + signDoc: SignDoc, + ): Promise { + if (this.chainId !== signDoc.chainId) { + throw new Error('Chain ID does not match signer chain ID'); + } + const accounts = await this.getAccounts(); + + if (accounts.find((account) => account.address !== signerAddress)) { + throw new Error('Signer address does not match wallet address'); + } + + return requestSignature( + this.chainId, + signerAddress, + signDoc, + ) as Promise; + } + + //This has been added as a placeholder. + async signAmino( + signerAddress: string, + signDoc: SignDoc, + ): Promise { + return this.signDirect( + signerAddress, + signDoc, + ) as unknown as Promise; + } +} + +export function getOfflineSigner(chainId: string) { + return new cosmjsOfflineSigner(chainId); +} diff --git a/packages/cosmos-snap-provider/src/index.ts b/packages/cosmos-snap-provider/src/index.ts new file mode 100644 index 0000000..138735b --- /dev/null +++ b/packages/cosmos-snap-provider/src/index.ts @@ -0,0 +1,3 @@ +export * from './snap'; +export * from './types'; +export * from './cosmjs-offline-signer'; diff --git a/packages/cosmos-snap-provider/src/snap.ts b/packages/cosmos-snap-provider/src/snap.ts new file mode 100644 index 0000000..bc551b6 --- /dev/null +++ b/packages/cosmos-snap-provider/src/snap.ts @@ -0,0 +1,133 @@ +import { AccountData } from '@cosmjs/amino'; +import { defaultSnapOrigin } from './config'; +import { GetSnapsResponse, Snap } from './types'; +import Long from 'long'; + +/** + * Get the installed snaps in MetaMask. + * + * @returns The snaps installed in MetaMask. + */ + +export const getSnaps = async (): Promise => { + return (await window.ethereum.request({ + method: 'wallet_getSnaps', + })) as unknown as GetSnapsResponse; +}; + +/** + * Connect a snap to MetaMask. + * + * @param snapId - The ID of the snap. + * @param params - The params to pass with the snap to connect. + */ +export const connectSnap = async ( + snapId: string = defaultSnapOrigin, + params: Record<'version' | string, unknown> = {}, +) => { + await window.ethereum.request({ + method: 'wallet_requestSnaps', + params: { + [snapId]: params, + }, + }); +}; + +/** + * Get the snap from MetaMask. + * + * @param version - The version of the snap to install (optional). + * @returns The snap object returned by the extension. + */ +export const getSnap = async (version?: string): Promise => { + try { + const snaps = await getSnaps(); + + return Object.values(snaps).find( + (snap) => + snap.id === defaultSnapOrigin && (!version || snap.version === version), + ); + } catch (e) { + console.log('Failed to obtain installed snap', e); + return undefined; + } +}; + +export const requestSignature = async ( + chainId: string, + signerAddress: string, + signDoc: { + bodyBytes?: Uint8Array | null; + authInfoBytes?: Uint8Array | null; + chainId?: string | null; + accountNumber?: Long | null; + }, +) => { + const signature = await window.ethereum.request({ + method: 'wallet_invokeSnap', + params: { + snapId: defaultSnapOrigin, + request: { + method: 'signDirect', + params: { + chainId, + signerAddress, + signDoc, + }, + }, + }, + }); + + const accountNumber = signDoc.accountNumber; + //@ts-ignore + const modifiedAccountNumber = new Long( + accountNumber!.low, + accountNumber!.high, + accountNumber!.unsigned, + ); + + const modifiedSignature = { + //@ts-ignore + signature: signature.signature, + signed: { + // @ts-ignore + ...signature.signed, + accountNumber: `${modifiedAccountNumber.toString()}`, + authInfoBytes: new Uint8Array( + //@ts-ignore + Object.values(signature.signed.authInfoBytes), + ), + + bodyBytes: new Uint8Array( + //@ts-ignore + Object.values(signature.signed.bodyBytes), + ), + }, + }; + + console.log('logging modified signature', modifiedSignature); + return modifiedSignature; +}; + +export const getKey = async (chainId: string): Promise => { + const accountData = await window.ethereum.request({ + method: 'wallet_invokeSnap', + params: { + snapId: defaultSnapOrigin, + request: { + method: 'getKey', + params: { + chainId, + }, + }, + }, + }); + + if (!accountData) throw new Error('No account data found'); + //@ts-ignore + accountData.pubkey = Uint8Array.from(Object.values(accountData.pubkey)); + + return accountData as AccountData; +}; + +export const isLocalSnap = (snapId: string) => snapId.startsWith('local:'); diff --git a/packages/cosmos-snap-provider/src/types.ts b/packages/cosmos-snap-provider/src/types.ts new file mode 100644 index 0000000..8f96603 --- /dev/null +++ b/packages/cosmos-snap-provider/src/types.ts @@ -0,0 +1,8 @@ +export type GetSnapsResponse = Record; + +export type Snap = { + permissionName: string; + id: string; + version: string; + initialPermissions: Record; +}; diff --git a/packages/cosmos-snap-provider/tsconfig.json b/packages/cosmos-snap-provider/tsconfig.json new file mode 100644 index 0000000..db60ef7 --- /dev/null +++ b/packages/cosmos-snap-provider/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + + "include": ["src"] +} diff --git a/packages/snap/snap.manifest.json b/packages/snap/snap.manifest.json index 374e312..15db10e 100644 --- a/packages/snap/snap.manifest.json +++ b/packages/snap/snap.manifest.json @@ -1,5 +1,5 @@ { - "version": "0.1.0", + "version": "0.1.1", "description": "Leap wallet sample snap", "proposedName": "Leap Wallet", "repository": { @@ -7,7 +7,7 @@ "url": "https://github.com/leapwallet/cosmos-metamask-snap.git" }, "source": { - "shasum": "9EeO5+1+eE7zUniDBU/5VM9ww6C1EsP8pE2pvYQZCks=", + "shasum": "N7FF/rqOb8EudU8zDolOwRBso/ShuzrUT6mE/x+K0Xw=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/yarn.lock b/yarn.lock index 4c3a7cd..7249656 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2158,6 +2158,12 @@ __metadata: languageName: unknown linkType: soft +"@leapwallet/cosmos-snap-provider@workspace:packages/cosmos-snap-provider": + version: 0.0.0-use.local + resolution: "@leapwallet/cosmos-snap-provider@workspace:packages/cosmos-snap-provider" + languageName: unknown + linkType: soft + "@leapwallet/metamask-cosmos-snap@workspace:packages/snap": version: 0.0.0-use.local resolution: "@leapwallet/metamask-cosmos-snap@workspace:packages/snap" From 41bdae918f9f51502133a78a788bae31bf882e81 Mon Sep 17 00:00:00 2001 From: baryon2 Date: Tue, 25 Jul 2023 17:03:57 +0200 Subject: [PATCH 2/2] Add cosmos-metamask-provider readme --- packages/cosmos-snap-provider/README.md | 49 +++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/packages/cosmos-snap-provider/README.md b/packages/cosmos-snap-provider/README.md index 03693e4..c8f44c9 100644 --- a/packages/cosmos-snap-provider/README.md +++ b/packages/cosmos-snap-provider/README.md @@ -1 +1,50 @@ # cosmos-snap-provider + +## Usage + +```typescript +import { getSnap, connectSnap, getKey } from '@leapwallet/cosmos-snap-provider' + +async function connect(){ + //check if snap is installed + const snapInstalled = await getSnap() + if(!snapInstalled) { + // Install snap if not already installed + connectSnap() + } + +} + +async function getAccount(){ + await connect() + const chainId = 'cosmoshub-4' + const key = await getKey(chainId) + return key +} + +``` + +## Usage with cosmjs + +```typescript +import { SigningStargateClient } from '@cosmjs/cosmwasm-stargate' +import { GasPrice } from '@cosmjs/stargate' + + +import { cosmjsOfflineSigner } from '@leapwallet/cosmos-snap-provider' + + + +const offlineSigner = new cosmjsOfflineSigner(chainId); +const accounts = await offlineSigner.getAccounts(); +const rpcUrl = "" // Replace with a RPC URL for the given chainId +const stargateClient = await SigningStargateClient.connectWithSigner( + rpcUrl, + offlineSigner, + { + gasPrice: GasPrice.fromString("0.0025ujuno"), + } +) + +``` +