Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,11 @@ jobs:
- uses: tetherto/oss-actions/node-base@v1
- name: Run tests
run: npm test
integration:
needs: lint
runs-on: ubuntu-latest
name: Integration
steps:
- uses: tetherto/oss-actions/node-base@v1
- name: Run integration tests
run: npm run test:integration
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,15 @@
"lint:fix": "standard --fix",
"sync-modules": "node scripts/sync-module-deps.mjs",
"typecheck": "tsc --noEmit -p jsconfig.json",
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
"test:coverage": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage"
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns tests/integration",
"test:integration": "cross-env NODE_OPTIONS=--experimental-vm-modules jest tests/integration --maxWorkers=4 --testTimeout=60000",
"test:coverage": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage --testPathIgnorePatterns tests/integration",
"test:integration:coverage": "cross-env NODE_OPTIONS=--experimental-vm-modules jest tests/integration --coverage --maxWorkers=4 --testTimeout=60000"
},
"jest": {
"setupFiles": [
"<rootDir>/tests/setup.js"
],
"testEnvironment": "node",
"testMatch": [
"<rootDir>/tests/**/*.test.js"
Expand Down
7 changes: 3 additions & 4 deletions src/config/wdk-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ const walletsFileRaw = createRequire(import.meta.url)('../../wdk.config.json')
* Per-token indexer slugs live in `wdk.tokens.json` under `metadata.slugs.indexer`.
* @property {string} [chainId] - The CAIP-2 chain id (e.g. "eip155:1", "tron:mainnet").
* @property {Record<string, unknown>} [config] - The per-network module configuration (RPC URL, chainId, etc.).
* @property {Record<string, Record<string, unknown>>} [providers] - Per-network provider config overrides,
* keyed by provider short name; shallow-merged over the provider's general `config`.
*/

/**
Expand Down Expand Up @@ -66,9 +64,10 @@ const walletsFileRaw = createRequire(import.meta.url)('../../wdk.config.json')
* @property {string} [module] - The protocol module package name; its version is pinned in
* `modules`. Absent for a provider the CLI calls directly rather than through a module.
* @property {Record<string, unknown>} [config] - General protocol config applied on every network
* (e.g. API keys); shallow-merged under any per-network override in `networks.<n>.providers.<name>`.
* (e.g. API keys); shallow-merged under any per-network override in `networks`.
* @property {Record<string, Record<string, unknown>>} [networks] - Per-network config overrides keyed by
* network name. Used by user-added providers, which cannot edit the packaged network entries.
* network name, shallow-merged over `config`. Everything a provider needs lives under the provider,
* so removing it takes its configuration with it.
* @property {string[]} [endpointKeys] - Config keys the module takes as a callback rather than a
* value. The CLI stores a URL for each and POSTs to it when the module calls back.
*/
Expand Down
18 changes: 15 additions & 3 deletions src/daemon/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -717,11 +717,25 @@ export class WalletDaemon {
}

/**
* Gracefully shuts down the daemon: disposes all wallets, closes the server, removes socket/PID files, and exits.
* Gracefully shuts down the daemon and exits the process. Split from
* {@link WalletDaemon#close} so a caller that owns the process — a test, or
* an embedder — can release the daemon's resources without terminating.
*
* @returns {Promise<void>}
*/
async shutdown () {
await this.close()
process.exit(0)
}

/**
* Releases everything the daemon holds: disposes each unlocked wallet, stops
* accepting connections, and removes the socket and PID files. Leaves the
* process running.
*
* @returns {Promise<void>}
*/
async close () {
for (const [, state] of this.#wallets) {
if (state.timer) clearTimeout(state.timer)
state.wdk.dispose()
Expand All @@ -746,8 +760,6 @@ export class WalletDaemon {
} catch {
/* */
}

process.exit(0)
}
}

Expand Down
22 changes: 22 additions & 0 deletions src/services/config-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,39 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { chmodSync } from 'node:fs'
import Conf from 'conf'
import { APP_NAME, CONFIG_DEFAULTS, getConfigDir } from '../config/constants.js'

/**
* Narrows the config directory to the owner. The files inside carry provider
* credentials and wallet names; without this another local account can read
* them. Windows has no POSIX mode and uses a per-user ACL instead, and a
* directory on a read-only mount cannot be changed — neither is worth failing
* a command over.
*
* @returns {void}
*/
function restrictConfigDir () {
if (process.platform === 'win32') return
try {
chmodSync(getConfigDir(), 0o700)
} catch {
// Best effort: the config itself is already written owner-only.
}
}

/** Persistent user-config store backed by `Conf`. */
class ConfigService {
constructor () {
this.conf = new Conf({
projectName: APP_NAME,
cwd: getConfigDir(),
// Owner-only: this file holds provider API keys in clear text.
configFileMode: 0o600,
defaults: CONFIG_DEFAULTS
})
restrictConfigDir()
}

/**
Expand Down
22 changes: 12 additions & 10 deletions src/services/protocol-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,9 @@ function userObject (key) {
}

/**
* Returns the networks that override something for a protocol: the packaged
* network entries naming it, the entry's own `networks`, and the networks the
* user configured under `providers.<name>.networks`.
* Returns the networks that override something for a protocol: the entry's own
* `networks`, and the networks the user configured under
* `providers.<name>.networks`.
*
* @param {string} name - The protocol short name.
* @param {WdkProtocolEntry} entry - The registry entry.
Expand All @@ -221,9 +221,6 @@ function userObject (key) {
export function getProviderNetworks (name, entry) {
/** @type {Set<string>} */
const names = new Set()
for (const [network, networkEntry] of Object.entries(walletsFile.networks)) {
if (hasOwn(networkEntry.providers, name)) names.add(network)
}
for (const network of Object.keys(entry.networks || {})) names.add(network)
for (const network of Object.keys(userObject(`providers.${name}.networks`))) names.add(network)
return [...names]
Expand All @@ -238,7 +235,7 @@ export function getProviderNetworks (name, entry) {
* Resolves a protocol's effective config, shallow-merging four layers with the
* later ones winning: the packaged general `config`, the user's
* `providers.<name>.config`, the packaged per-network override in
* `networks.<network>.providers.<name>` (or a user-added entry's own
* the entry's own `networks.<network>` (or
* `networks.<network>`, since it cannot edit the packaged network entries),
* and the user's `providers.<name>.networks.<network>`. Without a network only
* the two general layers apply.
Expand All @@ -262,8 +259,7 @@ export function resolveProtocolConfig (name, network, options = {}) {
const config = { ...(entry.config || {}), ...userObject(`providers.${name}.config`) }
if (network === undefined) return config

const packagedPerNetwork = getOwn(getOwn(walletsFile.networks, network)?.providers, name) ??
getOwn(entry.networks, network) ?? {}
const packagedPerNetwork = getOwn(entry.networks, network) ?? {}
return { ...config, ...packagedPerNetwork, ...userObject(`providers.${name}.networks.${network}`) }
}

Expand Down Expand Up @@ -360,7 +356,9 @@ export function saveCustomProvider (name, entry) {
}

/**
* Removes a user-added provider entry from config.
* Removes a user-added provider: its registry entry, its stored config, and
* any enabled/disabled override. Packaged providers are rejected, so their
* config is never touched.
*
* @param {string} name - The protocol short name.
* @returns {void}
Expand Down Expand Up @@ -388,6 +386,10 @@ export function removeCustomProvider (name) {
delete next[name]
if (Object.keys(next).length === 0) configService.delete('customProviders')
else configService.set('customProviders', next)
// Its config too, general and per-network. Left behind, it would be merged
// into the next provider registered under the same name — handing one
// service's credentials to another.
configService.delete(`providers.${name}`)
clearOverride('providers', name)
}

Expand Down
11 changes: 0 additions & 11 deletions src/ui/formatters.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,6 @@ export function formatNetworkLabel (network) {
return `${config.displayName} (${config.nativeSymbol})`
}

/**
* Formats an ISO 8601 date string into a locale-appropriate date/time string.
*
* @param {string} dateStr - ISO 8601 date string.
* @returns {string} Locale-formatted date string.
*/
export function formatDate (dateStr) {
const date = new Date(dateStr)
return date.toLocaleString()
}

/**
* Formats a token or native asset amount for display.
*
Expand Down
Loading
Loading