diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f042a4b..c30ff86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,8 +45,8 @@ jobs: - name: Create and fund an account run: | - go run ./cmd/golembase account create - go run ./cmd/golembase account fund + printf "password" | go run ./cmd/golembase account create + printf "password" | go run ./cmd/golembase account fund working-directory: ./gb-op-geth - name: Run tests diff --git a/README.md b/README.md index 9d8aa1e..e7a1a08 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The repo also contains an example application to showcase how you can use this S (Note: As an alternative to installing the demo CLI, you can build the [actual CLI](https://github.com/Golem-Base/golembase-op-geth/blob/main/cmd/golembase/README.md) as it's included in the golembase-op-geth repo.) -When you create a user, it will generate a private key file called `private.key` and store it in: +When you create a user, it will generate an encrypted keystore file with a password you provide called `wallet.json` and store it in: - `~/.config/golembase/` on **Linux** - `~/Library/Application Support/golembase/` on **macOS** @@ -126,7 +126,7 @@ This is a basic TypeScript application that: * `type GolemBaseCreate`: A type representing a create transaction in GolemBase * `Annotation`: A type representing an annotation with a key and a value, used for efficient lookups -2. Reads the private key, which it locates through the `xdg-portable` module. +2. Reads the private key from a wallet, which it locates through the `xdg-portable` module. 3. Create a logger using the `tslog` TypeScript Logger diff --git a/example/index.ts b/example/index.ts index 26023f4..6a7472c 100644 --- a/example/index.ts +++ b/example/index.ts @@ -1,9 +1,13 @@ -import * as fs from "fs" +import { readFileSync } from "fs" +import { join } from "path" +import { stdin, stdout } from "process" +import { createInterface } from "readline"; import { type ILogObj, Logger } from "tslog" import xdg from "xdg-portable" +import { Wallet, getBytes } from "ethers" import { createClient, formatEther, @@ -13,7 +17,39 @@ import { type AccountData, } from "golem-base-sdk" -const keyBytes = fs.readFileSync(xdg.config() + '/golembase/private.key'); +// Path to a golembase wallet +const walletPath = join(xdg.config(), 'golembase', 'wallet.json'); +const keystore = readFileSync(walletPath, 'utf8'); + +/** + * Read password either from piped stdin or interactively from the terminal. + */ +async function readPassword(prompt: string = "Enter wallet password: "): Promise { + if (stdin.isTTY) { + // Interactive prompt + const rl = createInterface({ + input: stdin, + output: stdout, + terminal: true, + }); + + return new Promise((resolve) => { + rl.question(prompt, (password) => { + rl.close(); + resolve(password.trim()); + }); + // Hide input for security + (rl as any)._writeToOutput = () => {}; + }); + } else { + // Input is piped + const chunks: Buffer[] = []; + for await (const chunk of stdin) { + chunks.push(Buffer.from(chunk)); + } + return Buffer.concat(chunks).toString().trim(); + } +} const encoder = new TextEncoder() const decoder = new TextDecoder() @@ -35,7 +71,11 @@ async function asyncFilter(arr: T[], callback: (item: T) => Promise) }; async function main() { - const key: AccountData = new Tagged("privatekey", keyBytes) + log.info("Attempting to decrypt wallet", walletPath); + const wallet = Wallet.fromEncryptedJsonSync(keystore, await readPassword()); + log.info("Successfully decrypted wallet for account", wallet.address); + + const key: AccountData = new Tagged("privatekey", getBytes(wallet.privateKey)) const client = { local: await createClient( 1337, diff --git a/package.json b/package.json index 486defa..4da0535 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "packageManager": "pnpm@10.5.2", "dependencies": { "@types/node": "^22.15.32", + "ethers": "^6.15.0", "tslog": "^4.9.3", "viem": "^2.31.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4a2bf8..0d790ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@types/node': specifier: ^22.15.32 version: 22.15.32 + ethers: + specifier: ^6.15.0 + version: 6.15.0 tslog: specifier: ^4.9.3 version: 4.9.3 @@ -66,6 +69,9 @@ importers: packages: + '@adraffy/ens-normalize@1.10.1': + resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} + '@adraffy/ens-normalize@1.11.0': resolution: {integrity: sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==} @@ -116,10 +122,17 @@ packages: resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} engines: {node: ^14.21.3 || >=16} + '@noble/curves@1.2.0': + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + '@noble/curves@1.9.2': resolution: {integrity: sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==} engines: {node: ^14.21.3 || >=16} + '@noble/hashes@1.3.2': + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} + engines: {node: '>= 16'} + '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -343,6 +356,9 @@ packages: '@types/node@22.15.32': resolution: {integrity: sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==} + '@types/node@22.7.5': + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -369,6 +385,9 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + aes-js@4.0.0-beta.5: + resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -504,6 +523,10 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + ethers@6.15.0: + resolution: {integrity: sha512-Kf/3ZW54L4UT0pZtsY/rf+EkBU7Qi5nnhonjUb8yTXcxH3cdcWrV2cRyk0Xk/4jK6OoHhxxZHriyhje20If2hQ==} + engines: {node: '>=14.0.0'} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -848,6 +871,9 @@ packages: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -870,6 +896,9 @@ packages: uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -900,6 +929,18 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.18.2: resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} engines: {node: '>=10.0.0'} @@ -947,6 +988,8 @@ packages: snapshots: + '@adraffy/ens-normalize@1.10.1': {} + '@adraffy/ens-normalize@1.11.0': {} '@cspotcode/source-map-support@0.8.1': @@ -1005,10 +1048,16 @@ snapshots: '@noble/ciphers@1.3.0': {} + '@noble/curves@1.2.0': + dependencies: + '@noble/hashes': 1.3.2 + '@noble/curves@1.9.2': dependencies: '@noble/hashes': 1.8.0 + '@noble/hashes@1.3.2': {} + '@noble/hashes@1.8.0': {} '@pkgjs/parseargs@0.11.0': @@ -1190,6 +1239,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@22.7.5': + dependencies: + undici-types: 6.19.8 + '@types/resolve@1.20.2': {} '@types/unist@3.0.3': {} @@ -1204,6 +1257,8 @@ snapshots: acorn@8.15.0: {} + aes-js@4.0.0-beta.5: {} + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} @@ -1305,6 +1360,19 @@ snapshots: estree-walker@2.0.2: {} + ethers@6.15.0: + dependencies: + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 22.7.5 + aes-js: 4.0.0-beta.5 + tslib: 2.7.0 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + eventemitter3@5.0.1: {} fdir@6.4.6(picomatch@4.0.2): @@ -1666,6 +1734,8 @@ snapshots: strip-bom: 3.0.0 optional: true + tslib@2.7.0: {} + tslib@2.8.1: {} tslog@4.9.3: {} @@ -1683,6 +1753,8 @@ snapshots: uc.micro@2.1.0: {} + undici-types@6.19.8: {} + undici-types@6.21.0: {} v8-compile-cache-lib@3.0.1: {} @@ -1722,6 +1794,8 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 + ws@8.17.1: {} + ws@8.18.2: {} xdg-portable@10.6.0: diff --git a/test/client.spec.ts b/test/client.spec.ts index a27a869..1014b83 100644 --- a/test/client.spec.ts +++ b/test/client.spec.ts @@ -1,4 +1,5 @@ -import * as fs from 'fs' +import { readFileSync } from "fs" +import { join } from "path" import { expect } from "chai" @@ -8,6 +9,7 @@ import { Logger } from "tslog" import xdg from "xdg-portable" +import { Wallet, getBytes } from "ethers" import { createClient, type GolemBaseClient, @@ -28,7 +30,12 @@ const log = new Logger({ minLevel: 3, }) -const keyBytes = fs.readFileSync(xdg.config() + '/golembase/private.key'); +// Path to a golembase wallet +const walletPath = join(xdg.config(), 'golembase', 'wallet.json'); +// The password that the test wallet was encrypted with +const walletTestPassword = "password"; +const keystore = readFileSync(walletPath, 'utf8'); +const wallet = Wallet.fromEncryptedJsonSync(keystore, walletTestPassword); let entitiesOwnedCount = 0 let entityKey: Hex = "0x" @@ -37,7 +44,7 @@ let client: GolemBaseClient describe("the golem-base client", () => { it("can be created", async () => { - const key: AccountData = new Tagged("privatekey", keyBytes) + const key: AccountData = new Tagged("privatekey", getBytes(wallet.privateKey)) client = { local: await createClient( 1337, diff --git a/test/internal/client.spec.ts b/test/internal/client.spec.ts index 550ee89..cc35c9d 100644 --- a/test/internal/client.spec.ts +++ b/test/internal/client.spec.ts @@ -1,4 +1,5 @@ -import * as fs from 'fs' +import { readFileSync } from "fs" +import { join } from "path" import { expect } from "chai" @@ -8,6 +9,7 @@ import { Logger } from "tslog" import xdg from "xdg-portable" +import { Wallet, getBytes } from "ethers" import { internal, type GolemBaseCreate, @@ -61,7 +63,13 @@ async function ownerAddress(client: internal.GolemBaseClient): Promise { return (await client.walletClient.getAddresses())[0] } -const keyBytes = fs.readFileSync(xdg.config() + '/golembase/private.key'); +// Path to a golembase wallet +const walletPath = join(xdg.config(), 'golembase', 'wallet.json'); +// The password that the test wallet was encrypted with +const walletTestPassword = "password"; +const keystore = readFileSync(walletPath); +const wallet = Wallet.fromEncryptedJsonSync(keystore, walletTestPassword); + let client: GolemBaseClient const data = generateRandomBytes(32) @@ -73,7 +81,7 @@ let expirationBlock: number describe("the internal golem-base client", () => { it("can be created", async () => { - const key: AccountData = new Tagged("privatekey", keyBytes) + const key: AccountData = new Tagged("privatekey", getBytes(wallet.privateKey)) client = { local: await internal.createClient( 1337,