Skip to content
This repository was archived by the owner on Oct 12, 2025. It is now read-only.

Commit 62f95e4

Browse files
committed
readme update
1 parent c2348ef commit 62f95e4

File tree

8 files changed

+223
-49
lines changed

8 files changed

+223
-49
lines changed

packages/snap/README.md

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,31 @@ yarn add @ecency/snap
1212
Add the snap to MetaMask using a local manifest or bundle produced by
1313
`yarn workspace @ecency/snap build`.
1414

15+
## Development
16+
17+
From the repository root you can build and test the snap:
18+
19+
```bash
20+
# compile TypeScript and generate the snap bundle/manifest
21+
yarn workspace @ecency/snap build
22+
23+
# run unit tests
24+
yarn workspace @ecency/snap test
25+
```
26+
27+
The build outputs `snap.manifest.json` alongside `dist/bundle.js`.
28+
29+
### Loading in MetaMask Flask
30+
31+
1. Install the [MetaMask Flask](https://metamask.io/flask/) extension.
32+
2. Run `yarn workspace @ecency/snap build` to generate the manifest and bundle.
33+
3. In MetaMask, open **Settings → Snaps** and choose **Add Snap**.
34+
4. Select `packages/snap/snap.manifest.json` from this repository.
35+
5. Approve the installation prompts.
36+
37+
The snap now appears in the MetaMask Snaps list and can be invoked by dApps
38+
using `local:@ecency/snap`.
39+
1540
## Usage
1641

1742
The snap exposes several RPC methods:
@@ -28,20 +53,39 @@ Example from a dApp:
2853

2954
```ts
3055
const result = await window.ethereum.request({
31-
method: 'wallet_invokeSnap',
56+
method: "wallet_invokeSnap",
3257
params: {
33-
snapId: 'local:@ecency/snap',
58+
snapId: "local:@ecency/snap",
3459
request: {
35-
method: 'getAddress',
36-
params: { chain: 'HIVE' }
37-
}
38-
}
60+
method: "getAddress",
61+
params: { chain: "HIVE" },
62+
},
63+
},
3964
});
4065
```
4166

42-
## Security considerations
67+
## Required Permissions
68+
69+
This snap declares the following permissions:
70+
71+
- `snap_getBip44Entropy` – derive Hive keys from the MetaMask seed phrase.
72+
- `endowment:rpc` – communicate with dApps via the snap RPC interface.
73+
- `snap_dialog` – prompt the user when signing transactions or encrypting and
74+
decrypting data.
75+
- `endowment:webassembly` – enable WebAssembly support for cryptographic
76+
libraries used by the snap.
77+
78+
All permissions follow the principle of least privilege. No private keys are
79+
stored in memory or exposed to the client.
80+
81+
## Security Notes
4382

44-
The mnemonic phrase is stored inside the snap's managed state. Snaps run in an
45-
isolated environment, but any state stored by the snap is persisted on the
46-
user's machine. Avoid exposing the mnemonic and consider encrypting state in
47-
production deployments.
83+
- Keys are derived only when required and cleared from memory immediately after
84+
use.
85+
- No network requests are made.
86+
- All transaction data is validated before processing.
87+
- No sensitive data is stored in browser storage.
88+
- The mnemonic phrase resides in the snap's managed state. Although snaps run in
89+
an isolated environment, that state persists on the user's machine. Avoid
90+
exposing the mnemonic and consider encrypting state for production
91+
deployments.

packages/snap/build-manifest.cjs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const fs = require("fs");
2+
const path = require("path");
3+
const crypto = require("crypto");
4+
5+
const pkg = JSON.parse(
6+
fs.readFileSync(path.join(__dirname, "package.json"), "utf8"),
7+
);
8+
const bundlePath = path.join(__dirname, "dist", "bundle.js");
9+
const source = fs.readFileSync(bundlePath);
10+
const shasum = crypto.createHash("sha256").update(source).digest("hex");
11+
12+
const manifest = {
13+
version: pkg.version,
14+
proposedName: "Ecency Snap",
15+
description: "MetaMask Snap providing multi-chain wallet capabilities.",
16+
repository: pkg.repository,
17+
source: {
18+
shasum,
19+
location: {
20+
local: "dist/bundle.js",
21+
},
22+
},
23+
initialPermissions: {
24+
snap_getBip44Entropy: [{ coinType: 756 }],
25+
"endowment:rpc": { dapps: true },
26+
snap_dialog: {},
27+
"endowment:webassembly": {},
28+
},
29+
manifestVersion: "0.1",
30+
};
31+
32+
fs.writeFileSync(
33+
path.join(__dirname, "snap.manifest.json"),
34+
JSON.stringify(manifest, null, 2),
35+
);

packages/snap/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
],
1313
"scripts": {
1414
"dev": "vite",
15-
"build": "tsc && vite build",
15+
"build": "tsc && vite build && node build-manifest.cjs",
1616
"test": "NODE_OPTIONS=--import=./test/mock-wallets.js node --test"
1717
},
1818
"dependencies": {

packages/snap/snap.manifest.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"version": "0.1.0",
3+
"proposedName": "Ecency Snap",
4+
"description": "MetaMask Snap providing multi-chain wallet capabilities.",
5+
"source": {
6+
"shasum": "7a7e92d25e072e85429c7288e9292058191192cac551341ae3fab60579154618",
7+
"location": {
8+
"local": "dist/bundle.js"
9+
}
10+
},
11+
"initialPermissions": {
12+
"snap_getBip44Entropy": [
13+
{
14+
"coinType": 756
15+
}
16+
],
17+
"endowment:rpc": {
18+
"dapps": true
19+
},
20+
"snap_dialog": {},
21+
"endowment:webassembly": {}
22+
},
23+
"manifestVersion": "0.1"
24+
}

packages/snap/test/integration.test.js

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,44 @@
1-
import test from 'node:test';
2-
import assert from 'node:assert/strict';
3-
import { onRpcRequest } from '../dist/ecency-snap.es.js';
1+
import test from "node:test";
2+
import assert from "node:assert/strict";
3+
import { onRpcRequest } from "../dist/bundle.js";
44

55
const state = {};
66

77
globalThis.snap = {
88
request: async ({ method, params }) => {
9-
if (method !== 'snap_manageState') throw new Error('unsupported');
10-
if (params.operation === 'get') return state;
11-
if (params.operation === 'update') { Object.assign(state, params.newState); return null; }
12-
throw new Error('bad operation');
9+
if (method !== "snap_manageState") throw new Error("unsupported");
10+
if (params.operation === "get") return state;
11+
if (params.operation === "update") {
12+
Object.assign(state, params.newState);
13+
return null;
14+
}
15+
throw new Error("bad operation");
1316
},
1417
};
1518

16-
const mnemonic = 'test test test test test test test test test test test junk';
19+
const mnemonic = "test test test test test test test test test test test junk";
1720

18-
test('dapp flow', async () => {
19-
await onRpcRequest({ origin: 'dapp', request: { method: 'initialize', params: { mnemonic } } });
20-
const addr = await onRpcRequest({ origin: 'dapp', request: { method: 'getAddress', params: { chain: 'HIVE' } } });
21+
test("dapp flow", async () => {
22+
await onRpcRequest({
23+
origin: "dapp",
24+
request: { method: "initialize", params: { mnemonic } },
25+
});
26+
const addr = await onRpcRequest({
27+
origin: "dapp",
28+
request: { method: "getAddress", params: { chain: "HIVE" } },
29+
});
2130
assert.ok(addr.address);
2231

23-
const tx = { ref_block_num: 0, ref_block_prefix: 0, expiration: '2020-01-01T00:00:00', operations: [], extensions: [] };
24-
const signed = await onRpcRequest({ origin: 'dapp', request: { method: 'signHiveTx', params: { tx } } });
32+
const tx = {
33+
ref_block_num: 0,
34+
ref_block_prefix: 0,
35+
expiration: "2020-01-01T00:00:00",
36+
operations: [],
37+
extensions: [],
38+
};
39+
const signed = await onRpcRequest({
40+
origin: "dapp",
41+
request: { method: "signHiveTx", params: { tx } },
42+
});
2543
assert.ok(signed.signatures.length > 0);
2644
});
27-

packages/snap/test/rpc.test.js

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,68 @@
1-
import test from 'node:test';
2-
import assert from 'node:assert/strict';
3-
import { onRpcRequest } from '../dist/ecency-snap.es.js';
1+
import test from "node:test";
2+
import assert from "node:assert/strict";
3+
import { onRpcRequest } from "../dist/bundle.js";
4+
import { getLastSignExternalTxParams } from "./mock-wallets.js";
45
import { getLastSignExternalTxParams } from './mock-wallets.js';
56

67
const state = {};
78

89
// simple snap state stub
910
globalThis.snap = {
1011
request: async ({ method, params }) => {
11-
if (method !== 'snap_manageState') throw new Error('unsupported');
12-
if (params.operation === 'get') return state;
13-
if (params.operation === 'update') {
12+
if (method !== "snap_manageState") throw new Error("unsupported");
13+
if (params.operation === "get") return state;
14+
if (params.operation === "update") {
1415
Object.assign(state, params.newState);
1516
return null;
1617
}
17-
throw new Error('bad operation');
18+
throw new Error("bad operation");
1819
},
1920
};
2021

21-
const mnemonic = 'test test test test test test test test test test test junk';
22+
const mnemonic = "test test test test test test test test test test test junk";
2223

23-
test('initialize and unlock', async () => {
24-
const init = await onRpcRequest({ origin: 'test', request: { method: 'initialize', params: { mnemonic } } });
24+
test("initialize and unlock", async () => {
25+
const init = await onRpcRequest({
26+
origin: "test",
27+
request: { method: "initialize", params: { mnemonic } },
28+
});
2529
assert.equal(init, true);
2630

27-
const unlock = await onRpcRequest({ origin: 'test', request: { method: 'unlock', params: { mnemonic } } });
31+
const unlock = await onRpcRequest({
32+
origin: "test",
33+
request: { method: "unlock", params: { mnemonic } },
34+
});
2835
assert.equal(unlock, true);
2936
});
3037

31-
test('get hive address', async () => {
32-
await onRpcRequest({ origin: 'test', request: { method: 'initialize', params: { mnemonic } } });
33-
const res = await onRpcRequest({ origin: 'test', request: { method: 'getAddress', params: { chain: 'HIVE' } } });
38+
test("get hive address", async () => {
39+
await onRpcRequest({
40+
origin: "test",
41+
request: { method: "initialize", params: { mnemonic } },
42+
});
43+
const res = await onRpcRequest({
44+
origin: "test",
45+
request: { method: "getAddress", params: { chain: "HIVE" } },
46+
});
3447
assert.ok(res.address);
3548
});
3649

37-
test('sign hive tx', async () => {
38-
await onRpcRequest({ origin: 'test', request: { method: 'initialize', params: { mnemonic } } });
39-
const tx = { ref_block_num: 0, ref_block_prefix: 0, expiration: '2020-01-01T00:00:00', operations: [], extensions: [] };
40-
const signed = await onRpcRequest({ origin: 'test', request: { method: 'signHiveTx', params: { tx } } });
50+
test("sign hive tx", async () => {
51+
await onRpcRequest({
52+
origin: "test",
53+
request: { method: "initialize", params: { mnemonic } },
54+
});
55+
const tx = {
56+
ref_block_num: 0,
57+
ref_block_prefix: 0,
58+
expiration: "2020-01-01T00:00:00",
59+
operations: [],
60+
extensions: [],
61+
};
62+
const signed = await onRpcRequest({
63+
origin: "test",
64+
request: { method: "signHiveTx", params: { tx } },
65+
});
4166
assert.ok(Array.isArray(signed.signatures));
4267
});
4368

@@ -51,9 +76,33 @@ test('sign external tx', async () => {
5176
assert.equal(getLastSignExternalTxParams().privateKey, 'priv');
5277
});
5378

54-
test('balance query placeholder', async () => {
55-
await onRpcRequest({ origin: 'test', request: { method: 'initialize', params: { mnemonic } } });
56-
const bal = await onRpcRequest({ origin: 'test', request: { method: 'getBalance', params: { currency: 'BTC', address: 'xyz' } } });
79+
test("balance query placeholder", async () => {
80+
await onRpcRequest({
81+
origin: "test",
82+
request: { method: "initialize", params: { mnemonic } },
83+
});
84+
const bal = await onRpcRequest({
85+
origin: "test",
86+
request: {
87+
method: "getBalance",
88+
params: { currency: "BTC", address: "xyz" },
89+
},
90+
});
5791
assert.equal(bal, 0);
5892
});
93+
test("sign external tx", async () => {
94+
await onRpcRequest({
95+
origin: "test",
96+
request: { method: "initialize", params: { mnemonic } },
97+
});
98+
const res = await onRpcRequest({
99+
origin: "test",
100+
request: {
101+
method: "signExternalTx",
102+
params: { currency: "BTC", params: { foo: "bar" } },
103+
},
104+
});
105+
assert.equal(res, "signed");
106+
assert.equal(getLastSignExternalTxParams().privateKey, "priv");
107+
});
59108

packages/snap/vite.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default defineConfig({
99
entry: path.resolve(__dirname, "src/index.ts"),
1010
name: "Ecency Snap",
1111
formats: ["es"],
12-
fileName: (format) => `ecency-snap.${format}.js`,
12+
fileName: () => "bundle.js",
1313
},
1414
rollupOptions: {
1515
external: [
@@ -24,7 +24,7 @@ export default defineConfig({
2424
"@okxweb3/coin-ton",
2525
"@okxweb3/coin-tron",
2626
"@okxweb3/crypto-lib",
27-
"bip39"
27+
"bip39",
2828
],
2929
},
3030
},

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4542,6 +4542,11 @@ typescript@5.8.2:
45424542
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4"
45434543
integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
45444544

4545+
typescript@^5.8.2:
4546+
version "5.9.2"
4547+
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6"
4548+
integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==
4549+
45454550
ufo@^1.5.4:
45464551
version "1.6.1"
45474552
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b"

0 commit comments

Comments
 (0)