Skip to content
This repository was archived by the owner on Oct 12, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ yarn-error.log*
# Misc
.DS_Store
*.pem
*.tsbuildinfo
*.tsbuildinfo
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
"dev": "turbo run dev",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"check-types": "turbo run check-types"
"check-types": "turbo run check-types",
"dev:snap": "turbo run dev --filter=@ecency/snap",
"dev:playground": "turbo run dev --filter=@ecency/snap-playground",
"dev:extras": "run-p dev:snap dev:playground"
},
"devDependencies": {
"prettier": "^3.5.3",
Expand Down
15 changes: 15 additions & 0 deletions packages/snap-playground/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# @ecency/snap-playground

Simple web app for installing and exercising the Ecency MetaMask Snap during
local development.

## Usage

```bash
yarn workspace @ecency/snap-playground start
```

Open `http://localhost:5173` in a MetaMask Flask-enabled browser and use the
page controls to install the snap, set a mnemonic and request derived addresses
for all supported chains. The playground performs the Hive RPC lookup so any
linked account name is displayed alongside the derived public keys.
16 changes: 16 additions & 0 deletions packages/snap-playground/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Ecency Snap Playground</title>
</head>
<body>
<h1>Ecency Snap Playground</h1>
<button id="connect">Install Snap</button>
<input id="mnemonic" placeholder="mnemonic" size="80" />
<button id="setMnemonic">Set Mnemonic</button>
<button id="getAddresses">Get Addresses</button>
<pre id="log"></pre>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
16 changes: 16 additions & 0 deletions packages/snap-playground/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@ecency/snap-playground",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "yarn workspace @ecency/snap dev & vite --port 5173 --strictPort",
"start": "yarn workspace @ecency/snap build && vite preview --port 5173 --strictPort",
"build": "yarn workspace @ecency/snap build && vite build"
},
"devDependencies": {
"typescript": "^5.8.2",
"vite": "^6.2.0",
"vite-plugin-node-polyfills": "^0.23.0"
}
}
80 changes: 80 additions & 0 deletions packages/snap-playground/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
const snapId = `local:${window.location.origin}/snap.manifest.json`;

async function connect() {
await (window as any).ethereum.request({
method: "wallet_requestSnaps",
params: { [snapId]: {} },
});
log("Snap installed");
}

async function invoke(method: string, params?: any) {
return (window as any).ethereum.request({
method: "wallet_invokeSnap",
params: {
snapId,
request: { method, params },
},
});
}

function log(msg: string) {
const el = document.getElementById("log");
if (el) el.textContent += `${msg}\n`;
}

async function lookupHiveAccount(pubkey: string): Promise<string | null> {
try {
const res = await fetch("https://api.hive.blog", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "account_by_key_api.get_key_references",
params: { keys: [pubkey] },
}),
});
const json = await res.json();
return json?.result?.accounts?.[0]?.[0] ?? null;
} catch {
return null;
}
}

document.getElementById("connect")?.addEventListener("click", () => {
connect().catch((err) => log(err.message));
});

document.getElementById("setMnemonic")?.addEventListener("click", async () => {
const mnemonic = (document.getElementById("mnemonic") as HTMLInputElement).value;
try {
await invoke("initialize", { mnemonic });
log("Mnemonic stored");
} catch (err) {
log((err as Error).message);
}
});

document.getElementById("getAddresses")?.addEventListener("click", async () => {
try {
const res = await invoke("getAddresses");
log(`BTC: ${res.btc}`);
log(`ETH: ${res.eth}`);
log(`APT: ${res.apt}`);
log(`TRX: ${res.trx}`);
log(`ATOM: ${res.atom}`);
log(`SOL: ${res.sol}`);
const account = await lookupHiveAccount(res.hive.active);
if (account) {
log(`HIVE account: ${account}`);
} else {
log(`HIVE owner: ${res.hive.owner}`);
log(`HIVE active: ${res.hive.active}`);
log(`HIVE posting: ${res.hive.posting}`);
log(`HIVE memo: ${res.hive.memo}`);
}
} catch (err) {
log((err as Error).message);
}
});
7 changes: 7 additions & 0 deletions packages/snap-playground/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src"
},
"include": ["src"]
}
12 changes: 12 additions & 0 deletions packages/snap-playground/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig } from "vite";
import { nodePolyfills } from "vite-plugin-node-polyfills";

export default defineConfig({
publicDir: "../snap",
server: {
fs: {
allow: [".."],
},
},
plugins: [nodePolyfills()],
});
145 changes: 145 additions & 0 deletions packages/snap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# @ecency/snap

MetaMask Snap providing multi-chain wallet capabilities powered by
[`@ecency/wallets`](../wallets).

## Installation

```bash
yarn add @ecency/snap
```

Add the snap to MetaMask using a local manifest or bundle produced by
`yarn workspace @ecency/snap build`.

## Development

From the repository root you can build and test the snap:

```bash
# compile TypeScript and generate the snap bundle/manifest
yarn workspace @ecency/snap build

# run unit tests
yarn workspace @ecency/snap test
```

The build outputs `snap.manifest.json` alongside `dist/bundle.js`.

### Loading in MetaMask Flask

1. Install the [MetaMask Flask](https://metamask.io/flask/) extension.
2. Run `yarn workspace @ecency/snap build` to generate the manifest and bundle.
3. In MetaMask, open **Settings → Snaps** and choose **Add Snap**.
4. Select `packages/snap/snap.manifest.json` from this repository.
5. Approve the installation prompts.

The snap now appears in the MetaMask Snaps list and can be invoked by dApps
using `local:@ecency/snap`.

### Playground

For a quick way to install and interact with the snap locally, start the
included playground which builds the snap and serves an example page:

```bash
yarn workspace @ecency/snap-playground start
```

Open `http://localhost:5173` in a MetaMask Flask-enabled browser and use the
page buttons to install the snap, set a mnemonic and request derived addresses
for all supported chains. The playground resolves any Hive account name on your
behalf and shows the public keys returned by the snap.

## Usage

The snap exposes several RPC methods:

- `initialize` – store a BIP39 mnemonic inside the snap.
- `unlock` – validate and unlock previously stored mnemonic.
- `getAddresses` – derive public keys for Hive roles and addresses for BTC,
ETH, APT, TRX, ATOM and SOL. dApps can perform any Hive account lookups
themselves using the returned keys.
- `signHiveTx` – sign a Hive transaction with the active key.
- `signExternalTx` – sign transactions for external chains via `signExternalTx` from
`@ecency/wallets`.
- `getBalance` – query balances using `useGetExternalWalletBalanceQuery` (todo: integrate).

### Connecting from a dApp

1. Request the snap to be installed in MetaMask. For local development use
`local:@ecency/snap`; when published to npm replace it with `npm:@ecency/snap` and an
optional version range.

```ts
await window.ethereum.request({
method: "wallet_enable",
params: [{
wallet_snap: {
"local:@ecency/snap": {}
}
}]
});
```

2. Invoke RPC methods exposed by the snap:

```ts
const result = await window.ethereum.request({
method: "wallet_invokeSnap",
params: {
snapId: "local:@ecency/snap",
request: { method: "getAddresses" }
}
});
// Optional: resolve Hive account name
const account = await lookupHiveAccount(result.hive.active);
```

3. Use the returned data in your application. When integrating into Ecency.com, these
calls can be wrapped in a connector module that detects MetaMask and manages snap
installation.

## Required Permissions

This snap declares the following permissions:

- `snap_getBip44Entropy` – derive Hive keys from the MetaMask seed phrase.
- `endowment:rpc` – communicate with dApps via the snap RPC interface.
- `snap_dialog` – prompt the user when signing transactions or encrypting and
decrypting data.
- `endowment:webassembly` – enable WebAssembly support for cryptographic
libraries used by the snap.

All permissions follow the principle of least privilege. No private keys are
stored in memory or exposed to the client.

## Security Notes

- Keys are derived only when required and cleared from memory immediately after
use.
- No network requests are made by the snap itself.
- All transaction data is validated before processing.
- No sensitive data is stored in browser storage.
- The mnemonic phrase resides in the snap's managed state. Although snaps run in
an isolated environment, that state persists on the user's machine. Avoid
exposing the mnemonic and consider encrypting state for production
deployments.

## Publishing & Discovery

To prepare the snap for public use:

1. **Build** the bundle and manifest:

```bash
yarn workspace @ecency/snap build
```

2. **Publish** the package to npm from `packages/snap`.

3. **Submit** the npm package to the [MetaMask Snaps Directory](https://docs.metamask.io/snaps/developing/register/) for
listing and discovery.

Once published, dApps like Ecency.com can enable the snap using the `npm:@ecency/snap`
identifier in the `wallet_enable` request.
41 changes: 41 additions & 0 deletions packages/snap/build-manifest.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");

const pkg = JSON.parse(
fs.readFileSync(path.join(__dirname, "package.json"), "utf8"),
);
const bundlePath = path.join(__dirname, "dist", "bundle.js");
// Compute the digest from the exact bundle bytes so the manifest always
// matches what MetaMask downloads over HTTP.
const source = fs.readFileSync(bundlePath);
const shasum = crypto.createHash("sha256").update(source).digest("base64");

const manifest = {
version: pkg.version,
proposedName: "Ecency Snap",
description: "MetaMask Snap providing multi-chain wallet capabilities.",
repository: pkg.repository,
source: {
shasum,
location: {
npm: {
filePath: "dist/bundle.js",
packageName: pkg.name,
registry: "https://registry.npmjs.org",
},
},
},
initialPermissions: {
snap_getBip44Entropy: [{ coinType: 756 }],
"endowment:rpc": { dapps: true },
snap_dialog: {},
"endowment:webassembly": {},
},
manifestVersion: "0.1",
};

fs.writeFileSync(
path.join(__dirname, "snap.manifest.json"),
JSON.stringify(manifest, null, 2),
);
25 changes: 25 additions & 0 deletions packages/snap/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@ecency/snap",
"private": false,
"version": "0.1.0",
"type": "module",
"license": "MIT",
"main": "dist/bundle.js",
"types": "dist/index.d.ts",
"files": ["dist", "snap.manifest.json", "README.md"],
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
"test": "NODE_OPTIONS=--import=./test/mock-wallets.js node --test"
},
"dependencies": {
"@ecency/wallets": "^1.3.11"
},
"devDependencies": {
"@types/node": "^22.13.8",
"typescript": "^5.8.2",
"vite": "^6.2.0",
"vite-plugin-dts": "4.5.3",
"vite-plugin-node-polyfills": "^0.23.0"
}
}
Loading
Loading